« SE3Groupe2024-2 » : différence entre les versions

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
 
(35 versions intermédiaires par 2 utilisateurs non affichées)
Ligne 2 : Ligne 2 :
Voici le lien git pour accéder aux différents fichiers relatifs à notre projet : https://gitea.plil.fr/ahouduss/se3_2024_B2.git
Voici le lien git pour accéder aux différents fichiers relatifs à notre projet : https://gitea.plil.fr/ahouduss/se3_2024_B2.git


== Description ==
== Description du projet ==


=== Objectif ===
=== Objectif ===
Ligne 140 : Ligne 140 :
|}
|}
''Datasheet ATmega32u4 :''
''Datasheet ATmega32u4 :''
[[Fichier:Datasheet ATMEGA32U4.pdf|199x199px|vignette|Datasheet du microcontroleur : ATMEGA32U4|centré]]
[[Fichier:Datasheet ATMEGA32U4.pdf|199x199px|vignette|Datasheet du microcontroleur : ATMEGA32U4|gauche]][[Fichier:AVR042.pdf|199x199px|vignette|AVR Hardware Design Considerations|centré]]


<p style="clear: both;" />
==== Communication ====
==== Communication ====
La station utilisera une puce '''NRF24L01''' pour la communication sans fil entre les différents actionneurs et capteurs.
La station utilisera une puce '''NRF24L01''' pour la communication sans fil entre les différents actionneurs et capteurs.
Ligne 174 : Ligne 176 :


Pour proteger les composants, nous allons ajouter un régulateur de tension pour garder une tension de 3,3V sur l'ensemble de notre carte.
Pour proteger les composants, nous allons ajouter un régulateur de tension pour garder une tension de 3,3V sur l'ensemble de notre carte.




Ligne 190 : Ligne 193 :


[[Fichier:Datasheet NHD‐C12832A1Z‐FSW‐FBW‐3V3.pdf|194x194px|vignette|Datasheet de l'écran : NHD‐C12832A1Z‐FSW‐FBW‐3V3|centré]]
[[Fichier:Datasheet NHD‐C12832A1Z‐FSW‐FBW‐3V3.pdf|194x194px|vignette|Datasheet de l'écran : NHD‐C12832A1Z‐FSW‐FBW‐3V3|centré]]
<p style="clear: both;" />On décide de prgrammer l'écran en C. On code donc notre écran via l'API "glcd.h".
<p style="clear: both;" />On décide de programmer l'écran en C. On code donc notre écran via l'API "glcd.h".


L'écran sera composé d'un menu permettant de naviguer parmi les différents capteurs enregistrés afin de consulter la valeur renvoyée par cle capteur choisi.
L'écran sera composé d'un menu permettant de naviguer parmi les différents capteurs enregistrés afin de consulter la valeur renvoyée par le capteur choisi.


Les boutons intégrés sur la carte ainsi que l'encodeur rotatif permettront à l'utilisateur de naviguer entre les différents capteurs..<p style="clear: both;" />
Les boutons intégrés sur la carte ainsi que l'encodeur rotatif permettront à l'utilisateur de naviguer entre les différents capteurs.<p style="clear: both;" />


==== Diverses ====
==== Diverses ====
Ligne 200 : Ligne 203 :
<p style="clear: both;" />
<p style="clear: both;" />


 
== Hardware ==
 
== Hardware de la station domotique ==


=== Schématique ===
=== Schématique ===
Ligne 214 : Ligne 215 :


=== Vue 3D ===
=== Vue 3D ===
[[Fichier:Station vue 3D ARRIERE.png|gauche|vignette|Carte station en 3D - Vue arrière]]
[[Fichier:Station vue 3D ARRIERE.png|gauche|vignette|Carte station en 3D - Vue arrière|461x461px]]
[[Fichier:Station vue 3D AVANT.png|centré|vignette|Carte station en 3D - Vue avant]]
[[Fichier:Station vue 3D AVANT.png|centré|vignette|Carte station en 3D - Vue avant|432x432px]]




Ligne 224 : Ligne 225 :
<p style="clear: both;" />
<p style="clear: both;" />


== Software de la station domotique ==
== Software ==
 
=== <big>Capteurs</big> ===
 
==== <big>Capteur de mouvement - HC-SR501</big> ====
 
===== Principe physique =====
Le '''capteur de mouvement HC-SR501''' est un '''capteur infrarouge''' passif (PIR), ce qui signifie qu’il ne produit aucun rayonnement mais détecte celui émis naturellement par les objets chauds, notamment le corps humain.
 
Ces deux cellules pyroélectriques sont disposées de manière à percevoir deux zones distinctes du champ de vision. En l'absence de mouvement, les deux reçoivent une quantité similaire d'infrarouge, et le signal reste équilibré.
 
Lorsqu'un corps chaud passe devant le capteur, la '''quantité d’infrarouge''' captée change '''entre les deux cellules''', '''créant un déséquilibre'''. Ce changement est interprété comme un mouvement.


=== Capteurs ===


==== Capteur de mouvement ====
Un dôme en plastique blanc recouvre le capteur : c’est une lentille de Fresnel.
[[Fichier:LentilleFresnel.png|centré|vignette|Lentille Fresnel du capteur de mouvement]]
 
Elle concentre et divise la lumière infrarouge en plusieurs zones, augmentant ainsi la portée et la sensibilité du capteur en "segmentant" son champ de vision. Ainsi, même un petit mouvement crée une variation significative de rayonnement perçu.


===== Spécifications techniques =====
===== Spécifications techniques =====
On utilise un capteur de mouvement HC-SR501 (voir datasheet ci-dessous) afin de détecter ou non la présence de quelqu'un dans une pièce et de pouvoir ensuite allumer la lumière si une personne est présente ou a contrario l'éteindre si la pièce est vide. Ici la lumière sera modélisée par une led présente sur l'arduino uno.  
On utilise un capteur de mouvement HC-SR501 (voir datasheet ci-dessous) afin de détecter ou non la présence de quelqu'un dans une pièce. L'intérêt est de pouvoir ensuite allumer la lumière si une personne est présente ou a contrario l'éteindre si la pièce est vide. Ici la lumière sera modélisée par une led présente sur l'arduino UNO en guise de démonstration.  
<p style="clear: both;" />
<p style="clear: both;" />


[[Fichier:Datasheet mvmt.pdf|alt=datasheet_mvmt|vignette|datasheet_mvmt]]  
[[Fichier:Datasheet mvmt.pdf|alt=datasheet_mvmt|vignette|datasheet_mvmt]]  
[[Fichier:Mvmt.png|alt=mvmt|vignette|capteur_mvmt|centré]]
[[Fichier:Mvmt.png|alt=mvmt|vignette|capteur_mvmt|479x479px|gauche]]


<p style="clear: both;" />
<p style="clear: both;" />
* Jumper Set
* Jumper Set


Les deux modes repeat ou single trigger permettent de régler le temps de détection avant d'arrêter de détecter une présence (par exemple, besoin d'une présence continue pour que la led reste allumée ou bien besoin de réactiver de temps à autre le capteur en bougeant).  
Les deux modes, repeat ou single trigger, permettent de régler le trigger de schmith permettant la détection d'une présence. En single, on effectue un seul trigger afin de détecter spontanément une présence (ex : Cas alarme intrusion). 
 
Dans l'autre mode on souhaite détecter un mouvement peu importe si celui-ci est déjà détecter. Par exemple, besoin d'un mouvement après un certains nombre de temps pour que la led reste allumée ou bien besoin de réactiver de temps à autre le capteur en bougeant.  


* Sensitivty Adjust
* Sensitivty Adjust


On modifie le potentiomètre à l'aide d'un tournevis afin d'ajuster la sensibilité à la présence de notre main, par exemple pour l'amplitude de nos mouvements.   
On modifie le potentiomètre à l'aide d'un tournevis afin d'ajuster la sensibilité à la présence de notre main, par exemple pour l'amplitude de nos mouvements.   
*Time Delay Adjust
Ici le potentiomètre permet d'ajuster le temps entre deux seuils de détection afin d'éviter la détection après des mouvements parasites, par exemple pour déclencher sans erreur une alarme intrusion. 


===== Circuit =====
===== Circuit =====
Ligne 251 : Ligne 271 :
===== Programmation =====
===== Programmation =====
Voici le code afin d'allumer une led dès qu'une présence est détectée. <syntaxhighlight lang="c" line="1">
Voici le code afin d'allumer une led dès qu'une présence est détectée. <syntaxhighlight lang="c" line="1">
/* ceci est le code pour le capteur de présence HC-SR501
* pour plus d'informations regarder dans le dossier datasheet pour la doc */
#include <avr/io.h>
#include <avr/io.h>
#include <util/delay.h>
#include <util/delay.h>
Ligne 265 : Ligne 282 :
             PORTB &= ~(1 << PB5); //led éteinte
             PORTB &= ~(1 << PB5); //led éteinte
         }
         }
         _delay_ms(500);
         _delay_ms(500); // Peut être baisser ou augmenter pour regler la sensibilité de détection
     }
     }
     return 0;
     return 0;
Ligne 278 : Ligne 295 :
<p style="clear: both;" />
<p style="clear: both;" />


Dans un second temps, on fera communiquer ce capteur avec notre carte station domotique par le biais des modules nrf24 afin d'afficher l'état de la pièce sur l'écran.
Dans un second temps, on fera communiquer ce capteur avec notre carte station domotique par le biais des modules de communication radio (NRF24L01) afin d'afficher l'état de la pièce sur l'écran et également transmettre ces données via le port série.
<p style="clear: both;" />
<p style="clear: both;" />


==== Capteur de température ====
==== <big>Capteur de température - DS18B20</big> ====
 
===== Principe physique =====
Ce capteur fonctionne grâce à un principe physique appelé '''variation de la résistance électrique avec la température'''. À l’intérieur du capteur, il y a un composant semi-conducteur, souvent une '''diode ou une jonction PN''', qui change son comportement électrique selon la température.
 
Quand la température augmente, la façon dont les électrons se déplacent dans ce matériau change, ce qui modifie la tension ou le courant électrique mesuré.
 
Un convertisseur analogique-numérique (CAN) est intégré au circuit afin de transferer par la suite la mesure via un protocole 1-Wire.
<p style="clear: both;" />


===== Spécifications techniques =====
===== Spécifications techniques =====
On utilise aussi le capteur de température DS18B20 (voir datasheet ci-dessous) afin de mesurer la température de l'eau (pour une piscine ou une plante par exemple).
On utilise aussi le capteur de température DS18B20 (voir datasheet ci-dessous) afin de mesurer la température dans une matière tel que l'eau ou bien la terre (pour une piscine ou une plante par exemple).


[[Fichier:Datasheet temp eau.pdf|alt=Datasheet_temp_eau|vignette|Datasheet_temp_eau|centré]]
[[Fichier:Datasheet temp eau.pdf|alt=Datasheet_temp_eau|vignette|Datasheet_temp_eau|centré]]
<p style="clear: both;" />


===== Circuit =====
===== Circuit =====
On branche les 3 broches de notre sonde de la manière suivante :
On branche les 3 broches de notre sonde de la manière suivante :


* le gnd est relié à la masse
* le GND est relié à la masse
* le power est relié au 3,3V
* le power est relié au 3,3V (peut également être relié sur le +5V si besoin)
* le fil de données est branché sur le pin PB
* le fil de données est branché sur un pin digital (valeur TOR) et ici sur PD2 (digital 2).


[[Fichier:Capteur eau.jpg|alt=capteur_eau|vignette|capteur_eau|centré]]
On doit brancher une résistance de 4,7kΩ entre le fil de données et le 3,3V (ou +5V).[[Fichier:Capteur eau.jpg|alt=capteur_eau|vignette|capteur_eau|centré]]
<p style="clear: both;" />
<p style="clear: both;" />


===== Programmation =====
===== Programmation =====


====== Arduino ======
====== Arduino ======
Pour commencer on a testé si notre sonde fonctionnait correctement à l'aide d'un code arduino et on a constaté aucun souci (voir dans le git).
Pour commencer, on a testé si notre sonde fonctionnait correctement à l'aide d'un code arduino et on a constaté aucun souci ([https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/Capteurs/Temperature%20eau/Arduino Code Arduino]).
====== C ======
====== C ======
On est donc passer à du code en c et on a trouvé plusieurs ressources sur github que l'on a fusionné afin d'obtenir un code fonctionnel.On a utilisé les fichiers du répertoire test1 :
Dans le cadre de ce projet, nous sommes partis d’un code initialement fonctionnel développé sur Arduino, puis nous l’avons adapté à notre propre environnement en langage C, plus proche du matériel et sans dépendance aux bibliothèques Arduino. Pour cela, nous avons fusionné deux ressources trouvées sur GitHub afin d'obtenir une base de code cohérente, fonctionnelle et surtout structurée de manière à répondre à nos besoins techniques.  


* onewire.h, onewire.c : pour remplacer la librairie OneWire.h de l'arduino afin de communiquer avec l'unique fil de données de la sonde.
Nous avons utilisé les fichiers disponibles dans le répertoire suivant : [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/Capteurs/Temperature%20eau/1er%20code%20%28C%29 Code C] 
* ds18b20.h, ds18b20.c : pour les fonctions princiales utiles à notre sonde.
 
* UART.h, UART.c : pour afficher la température sur la liaison série.
'''''<u>Protocole OneWire</u>''''' 
* main.c : pour le code principal
 
* Makefile : pour faciliter la compilation
Le '''protocole OneWire''' est un protocole de communication série développé par '''Maxim Integrated'''. Il permet à un microcontrôleur de communiquer avec un ou plusieurs périphériques (comme des capteurs de température, EEPROM, etc.) via '''un seul fil de données''' (en plus du GND). Ce fil est '''bidirectionnel''' et transporte à la fois les données et l’horloge synchronisée par le maître (généralement le microcontrôleur).
<p style="clear: both;" />
 
Ce protocole est particulièrement utilisé avec des capteurs qui mesurent la température et la transmettent sous forme numérique. Le principal avantage du OneWire est sa '''simplicité matérielle''' : un seul fil suffit pour communiquer avec plusieurs périphériques, chacun ayant une adresse unique codée en ROM.
 
Ce [https://blog.domadoo.fr/guides/principe-du-protocole-1-wire/ lien/] est un tutoriel qui nous explique comment fonctionne le protocole OneWire et sur [https://kampi.gitbook.io/avr/1-wire-implementation-for-avr ce lien] on retrouve un exemple de code complet mais pour notre usage nous nous sommes limités aux fonctionnalités essentielles ( à savoir écriture et lecture pour un unique appareil connecté).
 
<u>onewire.h, onewire.c </u>: pour remplacer la librairie OneWire.h de l'arduino afin de communiquer avec l'unique fil de données de la sonde en pure C.
 
* onewireInit : reset le bus de données et renvoie une erreur si le capteur de répond pas.
* onewireWriteBit : envoie un bit sur le bus de données en respectant le temps d'envoi du protocole Onewire.
* onewireWrite : transmet un octet en utilisant la fonction précédente.
* onewireReadbit : lit un bit sur le bus de données.
* onewireRead : lit un octet sur le bus de données.
<p style="clear: both;" /><p style="clear: both;" />'''''<u>Code de notre sonde de température</u>'''''
 
Nous avons étudier le fonctionnement de notre sonde de temperature via sa [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/99%20-%20Datasheets/Capteurs/DS18B20.pdf datasheet]. Nous nous sommes aidés de celle ci et de son code équivalent Arduino afin de pouvoir programmer une librairie en C (le [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/Capteurs/Temperature%20eau/1er%20code%20%28C%29/ds18b20.c fichier .c] et le [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/Capteurs/Temperature%20eau/1er%20code%20%28C%29/ds18b20.h fichier .h]).
 
<u>ds18b20.h, ds18b20.c</u> : pour les fonctions principales utiles à la communication entre notre sonde et notre microcontroleur.
* ds18B20crc8 : CRC signifie cyclic redundacy check est l'octet renvoyé par cette fonction qui permet de savoir si la transmission s'est effectuée sans erreurs.
* ds18b20match : utile si il y a plusieurs capteurs (pas le cas ici).
* ds18b20convert : la valeur de la température est stockée sur les deux premiers octets de la mémoire scratchpad. ds18b20convert permet de convertir ces octets en degré celsius.
* ds18b20rsp : lit le scratchpad (mémoire temporaire) pour récupérer la valeur de la température (sur les deux premiers octets).
* ds18b20wsp : écrit dans le scratchpad.
* ds18b20csp  : copie les données du scratchpad dans l'eeprom du capteur.
* ds18b20read : lit la température.
* ds18b20rom : lit l'adresse du capteur rom (pas utile ici car un seul capteur).<p style="clear: both;" />'''''<u>UART</u>'''''
 
Cette partie à été codé uniquement pour le debug car l'usage de l'UART sera négligé plus tard. Effectivement le but final c'est d'avoir un périphérique USB complet donc à coder via la LUFA.  Le port série virtuel USB (CDC) créé par LUFA est reconnu par la plupart des OS modernes sans besoin de drivers spécifiques. On aura alors un projet modulaire !<p style="clear: both;" />
 
<u>UART.h, UART.c</u> : pour afficher la température sur la liaison série. On définit deux fonctions :   
 
* USART_SendChar pour afficher un caractère sur le minicom. 
* USART_SendString pour afficher des mots sur le minicom. Rq : utiliser le retour chariot \r pour un affichage correct. 
<p style="clear: both;" /><p style="clear: both;" />'''''<u>Main</u>'''''
 
<u>main.c</u> : pour le code principal
<syntaxhighlight lang="c" line="1">
 
#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
#include "UART.h"
#include "ds18b20.h"
 
#define DS18B20_DDR  DDRD
#define DS18B20_PORT  PORTD
#define DS18B20_PIN  PIND
#define DS18B20_MASK  (1 << PD2)
 
int main(void)
{
    int16_t temperature_raw;
    char buffer[32];
    uint8_t error;
 
    USART_init(9600);
    USART_SendString("Debut lecture DS18B20...\r\n");
 
    while (1)
    {
        // Démarrer conversion
        error = ds18b20convert(&DS18B20_PORT, &DS18B20_DDR, &DS18B20_PIN, DS18B20_MASK, NULL);
        _delay_ms(800);  // attendre la fin de conversion
 
        if (error != DS18B20_ERROR_OK) {
            USART_SendString("Erreur conversion\r\n");
        }
        else {
 
            // Lire la température
            error = ds18b20read(&DS18B20_PORT, &DS18B20_DDR, &DS18B20_PIN, DS18B20_MASK, NULL, &temperature_raw);
            if (error == DS18B20_ERROR_OK) {
                float temperature_celsius = temperature_raw / 16.0;
                snprintf(buffer, sizeof(buffer), "Temp: %.2f C\r\n", temperature_celsius);
                USART_SendString(buffer);
            } else {
                snprintf(buffer, sizeof(buffer), "Erreur lecture: %d\r\n", error);
                USART_SendString(buffer);
            }
        }
        _delay_ms(200);
    }
    return 0;
}
 
</syntaxhighlight><p style="clear: both;" />Dans notre fonction main :
* on initialise la liaison série.
 
* on convertit les octets de la mémoire du capteur en une température en degré celsius.
* on lit la température afin de l'afficher dans le minicom. Pour cela, il faut au préalable convertir notre température en flottant en des caractères avec une taille adaptée au buffer à l'aide de la fonction snprintf (string numbered print format).
 
 
Le DS18B20 mesure la température avec une '''résolution de 0,0625 °C''', ce qui correspond à '''1/16 de degré Celsius'''. Si le capteur renvoyait directement la température en °C sous forme entière, il serait '''impossible d’exprimer des fractions précises''', comme 23,0625 °C.
 
En utilisant une '''valeur entière (int16_t)''' codant des '''fractions binaires''', on peut :
 
* Éviter les calculs en virgule flottante dans les systèmes embarqués (coûteux en ressources).
* Avoir une grande précision avec un codage simple :<blockquote>1 bit de poids faible = 0,0625 °C → résolution sur 12 bits.</blockquote>
 
'''''<u>Makefile</u>''''' :  <syntaxhighlight lang="makefile">
CC = avr-gcc
OBJCOPY = avr-objcopy
SIZE = avr-size
 
MCU = atmega328p
FCPU = 16000000UL
 
FLAGS = -mmcu=$(MCU) -Wl,-u,vfprintf -lprintf_flt -lm
CFLAGS = -Wall $(FLAGS) -DF_CPU=$(FCPU) -Os
LDFLAGS = $(FLAGS)
 
PROGRAMMER = avrdude
AVRDUDE_MCU = atmega328p
AVRDUDE_PORT = /dev/ttyACM0  # À adapter
AVRDUDE_BAUD = 115200
AVRDUDE_PROGRAMMER = arduino
 
TARGET = main
SOURCES = $(wildcard *.c)
OBJECTS = $(SOURCES:.c=.o)
 
all: $(TARGET).hex
 
clean:
rm -f *.o $(TARGET).hex $(TARGET).elf eeprom.hex
 
$(TARGET).elf: $(OBJECTS)
$(CC) -o $@ $^ $(LDFLAGS)
 
$(TARGET).hex: $(TARGET).elf
$(OBJCOPY) -j .text -j .data -O ihex $< $@
$(OBJCOPY) -j .eeprom --set-section-flags=.eeprom="alloc,load" \
--change-section-lma .eeprom=0 -O ihex $< eeprom.hex
 
upload: $(TARGET).hex
$(PROGRAMMER) -v -p $(AVRDUDE_MCU) -c $(AVRDUDE_PROGRAMMER) -P $(AVRDUDE_PORT) \
-b $(AVRDUDE_BAUD) -D -U flash:w:$(TARGET).hex:i
 
size: $(TARGET).elf
$(SIZE) --format=avr --mcu=$(MCU) $<
 
</syntaxhighlight>


===== Démonstration =====
===== Démonstration =====
Ligne 320 : Ligne 485 :
<p style="clear: both;" />
<p style="clear: both;" />


=== Actionneur ===
=== <big>Actionneur</big> ===
 
==== <big>Lumière</big> ====
On peut décider d'allumer une lumière selon certains critères (par exemple lorsque le capteur de présence détecte quelqu'un). Ici allumer une led par exemple. Cette partie n'as pas encore été mise en place, faute de temps. Nous avons préférez avancer sur l'écran avant tout.
 
=== <big>Ecran</big> ===
Pour offrir une interface utilisateur intuitive, nous avons décidé d’afficher sur un écran les données issues des capteurs ainsi que l’état des actionneurs. Par exemple, il doit être possible de visualiser la température d’une pièce ou la détection de présence en temps réel.
 
Nous avons choisi d’utiliser un écran [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/99%20-%20Datasheets/ECRAN_NHD%E2%80%90C12832A1Z%E2%80%90FSW%E2%80%90FBW%E2%80%903V3.pdf '''NHD‐C12832A1Z‐FSW‐FBW‐3V3'''] '''(stock de M. Boé)'''. Initialement, nous avons tenté d’utiliser la bibliothèque graphique '''u8g2''', réputée pour sa compatibilité avec de nombreux écrans. Cependant, malgré plusieurs essais de programmes issus de cette bibliothèque, l’écran restait vierge sans aucune information affichée.
 
Face à cette difficulté, nous avons décidé de simplifier notre approche en codant une instruction basique destinée à allumer l’ensemble des pixels de l’écran, en nous appuyant directement sur la datasheet du composant. Mais là encore, aucun résultat visible.
 
Cette absence de réaction nous a conduit à suspecter un problème matériel lié à la communication entre le microcontrôleur et l’écran. Nous avons alors découvert que notre écran ne fonctionne pas en I2C, contrairement à ce que nous avions initialement supposé, mais bien en '''SPI'''.
 
Pour corriger cela, nous avons modifié le câblage en coupant les pistes SDA et SCL (liées au bus I2C) puis connecté le pin SCL de l’écran au SCK du microcontrôleur, et le pin SI de l’écran au MOSI du microcontrôleur. 
[[Fichier:ModifPiste.jpg|centré|vignette|Modification de nos piste pour échnager avec l'écran|536x536px]] 
 
 
 
Malgré ces ajustements, l’écran restait toujours noir. Nous avons donc vérifié à l’oscilloscope la présence des signaux SPI transmis à l’écran, ce qui nous a confirmé que les données étaient bien envoyées. [[Fichier:Signaux SCL et MOSI.png|alt=signaux SCL et MOSI|centré|vignette|signaux SCL et MOSI|585x585px]]
 
Nous avons ensuite affiné notre code d’initialisation de l’écran, notamment en veillant à éteindre l’affichage pendant la configuration, puis à le rallumer une fois les paramètres correctement envoyés. Voici un extrait de la fonction d’initialisation :<syntaxhighlight lang="c" line="1">
void lcd_init() {
    lcd_reset();
    lcd_command(0xA0); // ADC select
    lcd_command(0xAE); // Display OFF
    lcd_command(0xC8); // COM direction scan
    lcd_command(0xA2); // LCD bias set
    lcd_command(0x2F); // Power Control set
    lcd_command(0x21); // Resistor Ratio Set
    lcd_command(0x81); // Electronic Volume Command (set contrast) Double Btye: 1 of 2
    lcd_command(0x20); // Electronic Volume value (contrast value) Double Byte: 2 of 2
    lcd_command(0xA6); // Display normal (non inverser)
    lcd_command(0xAF); // Display ON
}
 
</syntaxhighlight>
 
Nous avions également oublié de désactiver le JTAG afin de pouvoir utiliser correctement le PORTF.<syntaxhighlight lang="c" line="1">
MCUCR |= (1 << JTD);
MCUCR |= (1 << JTD);
</syntaxhighlight>
 
 
Nous avons également du allonger notre temps de delay pour notre fonction lcd_reset :<syntaxhighlight lang="c" line="1">
void lcd_reset() {
    PORTF &= ~(1 << RESET);
    _delay_ms(200);
    PORTF |= (1 << RESET);
    _delay_ms(200);
}
</syntaxhighlight>Grâce à ces modifications, nous avons réussi à allumer un carré de pixels sur l’écran. Nous nous sommes renseigné pour afficher des objet plus complexe tel qu'un logo. Nous avons récuperer le logo et le code pour afficher une image sur [https://support.newhavendisplay.com/hc/en-us/articles/4415264814231-NHD-C12832A1Z-with-Arduino le site du constructeur].
[[Fichier:Logo1.mp4|centré|vignette|Visualisation code écran avec logo]]
 
 
 
'''<u>Affichage d'une image</u>'''
 
Pour créer notre propre logo sous forme de tableau il faut s'aider de cette outil : https://javl.github.io/image2cpp/
 
Voici la configuration à adopter pour exporter correctement sur notre écran :
 
<u>''Image Settings''</u>
 
* Canvas size(s) = 128x32 (résolution de notre écran)
 
* Background color : Black (afin de n’afficher aucune couleur car écran monochrome
* Scaling : Scale to fit (pour redimensionner l’image selon nos nouvelles proportions)
 
Les autres paramètres de la section Image Settings restent inchangés (à l’exception de Center image, qui reste une option de personnalisation).
 
<u>''Output''</u>
 
* Code output format : plain bytes (pour obtenir uniquement les octets qui nous intéressent, on précisera nous-mêmes le type du tableau)
* Draw mode : Vertical - 1 bit per pixel (l’écran utilise un système de pages pour écrire les pixels)
 
[[Fichier:Logo2.mp4|centré|vignette|Visualisation code écran avec logo personnalisé]]
 
Et voici la fonction permettant d'afficher une image :<syntaxhighlight lang="c">
void DispPic(unsigned char* lcd_string)
{
    unsigned char page = 0xB0;
    lcd_command(0xAE); // Display OFF
    lcd_command(0x40); // Display start address + 0x40 (base RAM écran)
    for (unsigned int i = 0; i < 4; i++) { // Parcourt les 4 pages
        lcd_command(page); // Envoie l'adresse de la page actuelle (0xB0 + i)
        lcd_command(0x10); // column address upper 4 bits + 0x10
        lcd_command(0x00); // column address lower 4 bits + 0x00
        for (unsigned int j = 0; j < 128; j++){ // Parcourt toutes les colonnes (128 colonnes)
            lcd_data(*lcd_string); // Envoie un octet de données (une colonne verticale de 8 pixels)
            lcd_string++; // Passe à l'octet suivant dans lcd_string
        }
        page++; // after 128 columns, go to next page
    }
    lcd_command(0xAF);
}
</syntaxhighlight>'''<u>Affichage d'un texte</u>'''
 
Pour afficher du texte, nous avons créé un tableau appelé '''font bitmap''' en terme courant. Chaque caractère est représenté par une matrice de pixels 5x7, où chaque bit indique si un pixel doit être allumé ou non. Ce format compact nous permet d’afficher les lettres de manière claire et efficace, tout en s’adaptant à la taille souhaitée. Ici nous n'avons qu'une seule taille (5x7) pour répondre à l'objectif embarqué que nous nous sommes fixés (moindre code et moindre consommation).
 
Voici un tableau donnant une idée de ce à quoi cela peut ressembler :<syntaxhighlight lang="c" line="1">
const uint8_t font5x7[][5] = {
    // ASCII 32 à 127
    {0x00,0x00,0x00,0x00,0x00}, // (space)
    {0x00,0x00,0x5F,0x00,0x00}, // !
    [...]
    {0x00,0x60,0x60,0x00,0x00}, // .
    {0x20,0x10,0x08,0x04,0x02}, // /
    {0x3E,0x51,0x49,0x45,0x3E}, // 0
    {0x00,0x42,0x7F,0x40,0x00}, // 1
    {0x42,0x61,0x51,0x49,0x46}, // 2
    [...]
    {0x7E,0x11,0x11,0x11,0x7E}, // A
    [...]
}
</syntaxhighlight>Le tableau <code>font5x7</code> est organisé en '''5 colonnes par caractère''' parce que chaque caractère est représenté sur une matrice de pixels 5 colonnes (largeur) par 7 lignes (hauteur). Chaque caractère de la police bitmap fait '''5 pixels de large''' et '''7 pixels de haut'''. Ce genre de tableau peut être généré ou repris d'une librairie graphique tel que [https://github.com/andygock/glcd/blob/master/fonts/font5x7.h glcd].
 
Une fois que nous avons le tableau il suffit d'envoyer les données de celle ci ainsi : <syntaxhighlight lang="c" line="1">
void lcd_char(char c) {
    if (c == 248 || c == 176) { // '°' = ASCII étendu 248 ou parfois 176
        for (uint8_t i = 0; i < 5; i++) {
            lcd_data(deg_symbol[i]);
        }
        lcd_data(0x00); // espace
    }
    else if (c >= 32 && c <= 126) {
        for (uint8_t i = 0; i < 5; i++) {
            lcd_data(font5x7[c - 32][i]);
        }
        lcd_data(0x00); // espace
    }
}
 
void lcd_goto(uint8_t page, uint8_t column) {
    lcd_command(0xB0 | page);            // Page = 0 à 3
    lcd_command(0x10 | (column >> 4));  // MSB
    lcd_command(0x00 | (column & 0x0F)); // LSB
}
 
void lcd_print(char *str, uint8_t page, uint8_t column) {
    lcd_goto(page, column);
    while (*str) lcd_char(*str++);
}
</syntaxhighlight>L'envoie d'un espace après chaque envoie de caractère permet de creer un espacement '''entre les caractères affichés''', pour que les lettres '''ne soient pas collées''' les unes aux autres. '''L’écran est divisé en pages''' (souvent 8 pixels de hauteur par page, ici on a 4 pages si la hauteur est 32 pixel. Par exemple, <code>page 0</code> correspond à la ligne verticale 0–7, <code>page 1</code> à 8–15, etc...
 
'''La position horizontale se fait par colonnes''' (chaque colonne correspondant à une tranche verticale de pixels, souvent 1 octet = 8 pixels en hauteur).
 
Le contrôleur de l’écran LCD gère en interne une '''adresse mémoire d’écriture''' composée d’une page (ligne) et d’une colonne (position horizontale. Le contrôleur '''incrémente automatiquement la colonne''' pour la prochaine donnée.
 
* <code>lcd_goto()</code> sert à positionner le '''curseur initial'''.
 
* <code>lcd_data()</code> '''incrémente''' la colonne '''automatiquement''' après chaque octet envoyé.
[[Fichier:TexteLCD.jpg|centré|vignette|Photographie de l'écran avec du texte|519x519px]]
 
 
Ce projet nous a demandé beaucoup de temps et de persévérance, mais il nous a permis de comprendre en profondeur le fonctionnement d’un écran graphique. Nous sommes désormais capables de coder notre propre bibliothèque pour piloter l’écran, ce qui représentait auparavant un défi majeur.
 
=== <big>Communication</big> ===
Ici, nous traiterons du code implémenté afin de communiquer entre les différents capteurs/actionneurs et notre carte principale. Nous avons utilisé un NRF24L01 pour communiquer entre notre station domotique et nos capteurs/actionneurs. Voici le lien du tutoriel pour l’utilisation de puces à distance : [https://passionelectronique.fr/tutorial-nrf24l01 NRF24L01]
 
Ce tutoriel nous a aidés à tester ce module via [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/NRF24L01/Arduino Arduino]. Nous avons d'abord essayé de coder un simple "hello world" à envoyer et recevoir afin de comprendre le fonctionnement du NRF. Ce code permet également de tester les cartes aen cas de dysfonctionnement, comme c'était le cas avec un Arduino fourni par le professeur. Nous avons ensuite utilisé un Arduino qu’un d’entre nous possédait afin de coder.
[[Fichier:ArduinoDemo.mp4|centré|vignette]]
Par la suite, nous avons codé en C une bibliothèque pour le NRF permettant de communiquer avec celui-ci.
 
Le fichier [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/NRF24L01/C/01%20-%201er%20code%20sans%20opti/rx/nRF24L01.c .c] n'était pas fourni par [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/NRF24L01/C/99%20-%20nrf24L01_plus-master%20%28lib%20utilise%29 cette bibliothèque] et le fichier [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/NRF24L01/C/01%20-%201er%20code%20sans%20opti/rx/nRF24L01.h .h] a été complété par les fonctions ajoutées ci-dessous :<syntaxhighlight lang="c" line="1">
#include <avr/io.h>
#include "nRF24L01.h"
 
// Définition des ports et broches
#define MISO_DDR DDRB
#define MISO_PORT PORTB
#define MISO_PIN PINB
#define MISO_BIT PB4
 
#define MOSI_DDR DDRB
#define MOSI_PORT PORTB
#define MOSI_BIT PB3
 
#define SCK_DDR DDRB
#define SCK_PORT PORTB
#define SCK_BIT PB5
 
#define CSN_DDR DDRB
#define CSN_PORT PORTB
#define CSN_BIT PB2
 
#define CE_DDR DDRB
#define CE_PORT PORTB
#define CE_BIT PB1
 
// Initialisation des broches NRF24L01
void nrf24_setupPins() {
    // MISO en entrée
    MISO_DDR &= ~(1 << MISO_BIT);
 
    // MOSI, SCK, CSN, CE en sortie
    MOSI_DDR |= (1 << MOSI_BIT);
    SCK_DDR |= (1 << SCK_BIT);
    CSN_DDR |= (1 << CSN_BIT);
    CE_DDR |= (1 << CE_BIT);
 
    // Valeurs initiales : tout à 0 sauf CSN à 1 (désactivé)
    MOSI_PORT &= ~(1 << MOSI_BIT);
    SCK_PORT &= ~(1 << SCK_BIT);
    CE_PORT &= ~(1 << CE_BIT);
    CSN_PORT |= (1 << CSN_BIT);  // CSN HIGH (non sélectionné)
}
 
// Contrôle de la broche CE
void nrf24_ce_digitalWrite(uint8_t state) {
    if (state)
        CE_PORT |= (1 << CE_BIT);
    else
        CE_PORT &= ~(1 << CE_BIT);
}
 
// Contrôle de la broche CSN
void nrf24_csn_digitalWrite(uint8_t state) {
    if (state)
        CSN_PORT |= (1 << CSN_BIT);
    else
        CSN_PORT &= ~(1 << CSN_BIT);
}
 
// Contrôle de la broche SCK
void nrf24_sck_digitalWrite(uint8_t state) {
    if (state)
        SCK_PORT |= (1 << SCK_BIT);
    else
        SCK_PORT &= ~(1 << SCK_BIT);
}
 
// Contrôle de la broche MOSI
void nrf24_mosi_digitalWrite(uint8_t state) {
    if (state)
        MOSI_PORT |= (1 << MOSI_BIT);
    else
        MOSI_PORT &= ~(1 << MOSI_BIT);
}
 
// Lecture de la broche MISO
uint8_t nrf24_miso_digitalRead() {
    return (MISO_PIN & (1 << MISO_BIT)) ? 1 : 0;
}
 
</syntaxhighlight>Nous avons déclaré en <code>#define</code> les DDR, PORT, PIN et BIT de chaque broche afin d'avoir un code plus lisible.
 
'''<u>Initialisation des broches du NRF</u>'''
 
Pour commencer, nous avons <code>nrf24_init()</code> qui sert à configurer les broches utilisées par le module (MISO, MOSI, SCK, CSN, CE). Ça permet de préparer la communication SPI logicielle. Ensuite, on mets CE à LOW et CSN à HIGH, ce qui correspond à l’état « repos » du module.
 
 
'''<u>Configuration du module NRF</u>'''La fonction <code>nrf24_config</code> sert à configurer le module selon le canal radio (fréquence) et la taille des paquets (payload).
 
* Elle la longueur de la charge utile (payload) dans une variable globale.
* Elle configure les différents registres : le canal RF, la taille du payload pour les pipes (canaux de réception), la puissance d’émission, le CRC, l’auto-acknowledgment (reconnaissance automatique de réception), les adresses RX activées, la retransmission automatique.
* Puis elle mets le module en mode écoute (réception).
 
 
 
'''<u>Gestion des adresses TX et RX</u>'''
 
<code>nrf24_tx_address()</code> et <code>nrf24_rx_address()</code> servent à définir les adresses pour l’envoi et la réception. Ces adresses doivent être cohérentes pour que la communication fonctionne.
'''<u>Envoi et réception des données</u>'''
 
'''<u>Envoi et réception des données</u>'''
 
<code>nrf24_send()</code> permet d’envoyer un paquet. Elle prépare le module, vide le FIFO d’émission, puis écris le payload et démarre la transmission.
 
<code>nrf24_getData()</code> lit les données reçues depuis le module en SPI, puis remet à zéro le flag d’interruption réception.
 
 
'''<u>Vérification de l’état</u>'''
 
<code>nrf24_dataReady()</code> et <code>nrf24_rxFifoEmpty()</code> permettent de savoir si des données sont prêtes à être lues.
 
<code>nrf24_isSending()</code> indique si le module est encore en train d’envoyer un message.
 
<code>nrf24_lastMessageStatus()</code>  dit si la dernière transmission a réussi ou a échoué (nombre max de retransmissions atteint).
 
<code>nrf24_retransmissionCount()</code> donne le nombre de tentatives de retransmission.
 
 
'''<u>Gestion de la puissance</u>'''
 
<code>nrf24_powerUpRx()</code>, <code>nrf24_powerUpTx()</code>, <code>nrf24_powerDown()</code> sont des fonctions pour mettre le module en mode réception, émission, ou veille.
 
 
 
'''<u>Communication SPI en logiciel (bit-banging)</u>'''
 
Comme on n’utilise pas le matériel SPI natif, <code>spi_transfer()</code> envoie et reçoit un octet via manipulation manuelle des broches MOSI, MISO et SCK.
 
Les fonctions <code>nrf24_transferSync()</code> et <code>nrf24_transmitSync()</code> permettent d’envoyer ou recevoir plusieurs octets à la suite.
Cela à été fait de cette façon afin d'avoir le code le plus portatif possible, ce qui explique le contenu de cette librairie.
Nous n'avons pas implémenté le SPI matériel puisque le code fonctionne très bien sans.
'''<u>Lecture/écriture des registres</u>'''
Pour lire ou écrire un registre du nRF24, <code>nrf24_readRegister()</code> et <code>nrf24_writeRegister()</code>, envoient la commande adéquate en SPI puis récupèrent ou envoient les données.
 
 
Nous avons commencé par étudier la documentation officielle du module '''nRF24L01''', en particulier le '''datasheet''', afin de comprendre le protocole SPI, les registres internes et les commandes à utiliser. Ensuite, nous avons consulté différents exemples sur '''GitHub''' ainsi que des tutoriels pour Arduino.
 
 
Je ne vais pas remontrer la vidéo de démonstration c'est redondant ici. Il y en aura une pour la prochaine étape qui est ...
 
=== IHM PC ===
Dans cette section, nous expliquons comment la '''carte domotique''' communique avec un '''PC via USB''', en utilisant la bibliothèque '''LUFA'''. Le code utilisé est un exemple issu du  [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/01%20-%20Programmateur%20AVR/02%20-%20Programmation/lufa-LUFA-210130-NSI/Demos/Device/LowLevel/VirtualSerial repertoire suivant], déjà employé dans un projet annexe de '''programmateur AVR'''. Ce code permet l’envoi simple de données via une '''liaison série USB''' (USB CDC). 
 
Pour récupérer ces données côté PC et les afficher, nous avons choisi d’utiliser d’abord '''Node-RED''' pour la gestion des flux de données, puis '''Grafana''' (outil recommandé par M. Boé) pour l’affichage graphique qui sera implémenté plus tard. Ce choix nous permet de gagner du temps sur la partie interface web, qui peut être longue à développer manuellement. 
 
Grafana ne sera peut être pas déployé car la liaison entre Node-RED et Grafana doit se faire depuis une base de donnée (surement InfluxDB) et cela prend du temps à être mis en place. 
 
Voici le repertoire de cette partie : https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/03%20-%20%20ui_web_interface 
 
=== LUFA ===
Ce code permet de faire un Hello World. Il a été nettoyé au préalable. Il est également disponible [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/CapteurHumidite_ET_NRF/Lufa/SE/HelloWorld dans ce repertoire].<syntaxhighlight lang="c">
#include "VirtualSerial.h"
 
static CDC_LineEncoding_t LineEncoding = { .BaudRateBPS = 0,
                                          .CharFormat  = CDC_LINEENCODING_OneStopBit,
                                          .ParityType  = CDC_PARITY_None,
                                          .DataBits    = 8 };
 
int main(void)
{
    SetupHardware();
    LEDs_SetAllLEDs(LEDMASK_USB_NOTREADY);
    GlobalInterruptEnable();
 
    for (;;)
    {
        CDC_Task();
        USB_USBTask();
    }
}
 
void SetupHardware(void)
{
    MCUSR &= ~(1 << WDRF);
    wdt_disable();
    clock_prescale_set(clock_div_1);
 
    USB_Init();
}
 
void EVENT_USB_Device_Connect(void)
{
    LEDs_SetAllLEDs(LEDMASK_USB_ENUMERATING);
}
 
void EVENT_USB_Device_Disconnect(void)
{
    LEDs_SetAllLEDs(LEDMASK_USB_NOTREADY);
}
 
void EVENT_USB_Device_ConfigurationChanged(void)
{
    bool ConfigSuccess = true;
    ConfigSuccess &= Endpoint_ConfigureEndpoint(CDC_NOTIFICATION_EPADDR, EP_TYPE_INTERRUPT, CDC_NOTIFICATION_EPSIZE, 1);
    ConfigSuccess &= Endpoint_ConfigureEndpoint(CDC_TX_EPADDR, EP_TYPE_BULK, CDC_TXRX_EPSIZE, 1);
    ConfigSuccess &= Endpoint_ConfigureEndpoint(CDC_RX_EPADDR, EP_TYPE_BULK, CDC_TXRX_EPSIZE, 1);
    LineEncoding.BaudRateBPS = 0;
    LEDs_SetAllLEDs(ConfigSuccess ? LEDMASK_USB_READY : LEDMASK_USB_ERROR);
}
 
void EVENT_USB_Device_ControlRequest(void)
{
    switch (USB_ControlRequest.bRequest)
    {
        case CDC_REQ_GetLineEncoding:
            if (USB_ControlRequest.bmRequestType == (REQDIR_DEVICETOHOST | REQTYPE_CLASS | REQREC_INTERFACE))
            {
                Endpoint_ClearSETUP();
                Endpoint_Write_Control_Stream_LE(&LineEncoding, sizeof(LineEncoding));
                Endpoint_ClearOUT();
            }
            break;
        case CDC_REQ_SetLineEncoding:
            if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
            {
                Endpoint_ClearSETUP();
                Endpoint_Read_Control_Stream_LE(&LineEncoding, sizeof(LineEncoding));
                Endpoint_ClearIN();
            }
            break;
        case CDC_REQ_SetControlLineState:
            if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
            {
                Endpoint_ClearSETUP();
                Endpoint_ClearStatusStage();
            }
            break;
    }
}
 
void CDC_Task(void)
{
    // Vérifie si le périphérique USB est bien configuré avant de continuer
    if (USB_DeviceState != DEVICE_STATE_Configured)
        return;
 
    // Chaîne de caractères à envoyer sur la liaison série USB
    char msg[] = "Hello world\r\n";
 
    // Sélectionne l'endpoint d'envoi (TX) pour préparer l'envoi de données
    Endpoint_SelectEndpoint(CDC_TX_EPADDR);
 
    // Écrit le message dans le buffer de l'endpoint USB, en mode Little Endian
    Endpoint_Write_Stream_LE(msg, sizeof(msg)-1, NULL);
 
    // Vérifie si le buffer de l'endpoint est plein après l'écriture
    bool full = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);
 
    // Vide (envoie) le contenu du buffer IN vers l'hôte
    Endpoint_ClearIN();
 
    // Si le buffer était plein, attend que l'endpoint soit prêt pour un autre envoi
    if (full)
    {
        // Attend que l'endpoint soit prêt pour un nouvel envoi (acknowledgement de l'hôte)
        Endpoint_WaitUntilReady();
 
        // Vide de nouveau le buffer pour s'assurer que tout est bien envoyé
        Endpoint_ClearIN();
    }
 
    // Sélectionne l'endpoint de réception (RX) pour traiter d'éventuelles données entrantes
    Endpoint_SelectEndpoint(CDC_RX_EPADDR);


==== Lumière ====
    // Si des données ont été reçues par l'hôte on vide le buffer
On peut décider d'allumer une lumière selon certains critèrres (par exemple lorsque le capteur de présence détecte quelqu'un). Ici allumer une led par exemple.
    if (Endpoint_IsOUTReceived())
=== Communication ===
        Endpoint_ClearOUT();
Pour plus de détails, se référer à la rubrique spécifications techniques->communication. Ici nous traiterons du code implémenté afin de communiquer entre les différents capteurs/actionneurs et notre carte principale.
=== Ecran ===
Afin d'avoir une interface pour l'utilisateur, nous décidons d'afficher sur un écran les données reçues des capteurs et l'état des actionneurs. On doit donc voir s'afficher la température d'une pièce, si il y a une présence etc.


Pour cela, on va devoir coder l'écran NHD‐C12832A1Z‐FSW‐FBW‐3V3 (voir datasheet dans la rubrique spécifications techniques->écran).
    // Pause de 300 ms pour ralentir l'exécution de la tâche (évite les envois en boucle trop rapides)
== Programmateur AVR ==
    _delay_ms(300);
}
 
 
</syntaxhighlight>Nous n'avons pas réussi à souder notre NRF, malheureusement le code avec celui ne fonctionnera donc pas. Le code avec NRF sera fait avec un Arduino Uno et UART comme l'atmega328p ne supporte pas la LUFA.
 
Voici tout de même une piste qui pourrait fonctionner : https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/CapteurHumidite_ET_NRF/Lufa/SE/VirtualSerial
 
=== '''Docker''' ===
Ces outils sont déployés à l’aide de '''Docker''', une technologie (open source et créée par des ingénieurs français !)  de virtualisation légère qui permet d’exécuter des applications dans des conteneurs isolés. 
 
Docker permet de '''packager toute une application et ses dépendances''' dans un conteneur. Plus besoin de réinstaller des bibliothèques, configurer l’environnement, ou se soucier du “ça marche sur mon PC mais pas ailleurs”.  Cela marchera donc aussi de votre côté ;).   
 
Nous utilisons également '''Docker Compose''' pour automatiser le lancement coordonné de plusieurs services (ici Node-RED et Grafana) à partir d’un simple fichier de configuration. 
 
Voici le fichier de configuration de '''Docker Compose''' :  <syntaxhighlight lang="yaml">
version: '3.8'
 
services:
 
  nodered:
    image: nodered/node-red:latest
    container_name: nodered
    ports:
      - "1880:1880"
    volumes:
      - ./nodered_data:/data
    devices:
      - /dev/ttyACM0
    restart: unless-stopped
 
  grafana:
    image: grafana/grafana-oss
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - ./grafana_data:/var/lib/grafana
 
 
    restart: unless-stopped
 
</syntaxhighlight>On configure le port où le service sera lancé et nous laissons les accès devices à nodered pour quelle puisse écouter le port série où notre LUFA écrit. La mention <code>- ${SERIAL_DEV:-/dev/null}:/dev/ttyACM0</code>  peut être ajouté afin de pouvoir lancer le dock sans problème de compilation car Docker ne démarre pas le conteneur si un périphérique mentionné dans <code>devices:</code> est '''introuvable.'''
 
La mention<code>${SERIAL_DEV:-/dev/null}:</code> créera un lien vers <code>/dev/null</code> (un périphérique vide), évitant ainsi l’erreur. Je connaissais Docker parceque j'utilisais une application qui se lançait sur celle-ci et je m'y suis intéressé. J'estimais intéressant de l'intégrer au projet !
 
Voici un fichier mémo qui nous a aider à nous rappeler des commandes importantes sur Docker :<syntaxhighlight lang="text">
Liste des docker actif sur le pc :
docker ps -a
 
Dans ce repertoire, lancer les dockers via :
docker compose up -d
 
Pour relancer copie tout :
docker compose down
docker compose up -d
 
Pour stopper un docker :
docker stop <nomDock>
 
Pour supprimer un docker :
docker rm -f <nomDock>
 
Si un pb, voir log :
docker logs <nomDock>
 
</syntaxhighlight>
 
=== '''Node-RED''' ===
Voici à quoi ressemble notre configuration :
[[Fichier:NodeRedConfig.png|centré|vignette|778x778px|Configuration Node-RED]]
 
 
'''Bloc 1 : Entrée série (Serial In)'''
 
Ce bloc permet d'écouter un port série. Il lit les données envoyées par la carte domotique sur le port <code>/dev/ttyACM0</code>. Les données sont transmises sous forme de texte brut, souvent une chaîne JSON. Voici la configuration :
[[Fichier:ConnfigLEcturePortSerie.png|centré|vignette|596x596px|Configuration bloc 1]]
 
 
'''Bloc 2 : Conversion JSON'''
 
Les données reçues sont des chaînes de caractères au format JSON. Ce bloc les convertit en objet JavaScript pour que Node-RED puisse les manipuler plus facilement dans les blocs suivants.
 
 
'''Bloc 3 : Traitement de la donnée (Function)'''
 
Ce bloc exécute une petite fonction JavaScript pour isoler la température contenue dans l'objet JSON.
 
Voici le contenu de la fonction :<syntaxhighlight lang="js" line="1">
msg.payload = msg.payload.temperature;
return msg;
</syntaxhighlight>
 
* <code>msg.payload</code> contient l'objet JSON complet, par exemple :  <code>{ "temperature": 22.5, "...": 45, "...": 20, "...": "oui" }</code>
* La ligne <code>msg.payload = msg.payload.temperature;</code> remplace le contenu du message pour ne garder que la valeur de la température (ici <code>22.5</code>). Cela permet d’envoyer uniquement la température au bloc suivant, comme une simple valeur numérique.
 
* <code>return msg;</code> renvoie ce nouveau message modifié pour qu’il continue à circuler dans le flow.
 
 
'''Bloc 4 : Affichage ou base de données'''
 
Le message contenant uniquement la température peut ensuite être affiché dans un tableau de bord, envoyé à Grafana, ou enregistré dans une base de données.
 
Pour le moment on utilisera pas de base donnée mais une interface beaucoup plus minimaliste sur Node-RED.
 
Voici la configuration du bloc :
[[Fichier:CaptureConfigDataDisplay.png|centré|vignette|573x573px|Configuration bloc 3 permettant l'affichage de la température]]
Pour avoir accès au noeud du bloc 1 et 3 il a fallu ajouter des "nodes" à notre palette. Pour y acceder c'est ici :
[[Fichier:PaletteNodeAccess.png|centré|vignette|Acces à Palette ]]
Et voici les noeuds installés :
[[Fichier:PaletteNoodes.png|centré|vignette|408x408px|Palette de nodes du projet]]
 
 
Maintenant regardons notre site final :
[[Fichier:AccesVueRedNode.png|gauche|vignette|249x249px|Accès au résultat de notre Node-RED]]
[[Fichier:Vue sur notre projet Node RED.png|centré|vignette|301x301px|Accès vue sur notre projet]]
 
 
 
 
 
 
 
 
On clique sur la petite icône est nous sommes renvoyé sur cette url : http://localhost:1880/ui/
 
Voici notre interface finale :
[[Fichier:Dashboard.png|centré|vignette|1456x1456px|Dashboard site web]]
 
Et une démonstration du projet dans son intégralité en vidéo ci dessous :
[[Fichier:VideoDemoFinalv1.mp4|centré|vignette|Vidéo demonstration du projet fonctionnel]]
 
=== Programmateur AVR (Projet annexe) ===


=== Objectif ===
=== Objectif ===
Réaliser un programmateur AVR afin d'envoyer notre code C sur le microcontrôleur de notre carte.
Réaliser un programmateur AVR afin d'envoyer notre code C sur un microcontrôleur. Ce projet est une introduction au logiciel et à la programmation sur microcontroleur AVR.
=== Cours / Tutoriel ===
 
https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme063.html
Nous nous sommes aider du cours afin de réaliser notre projet : https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme063.html
=== Schématique ===
=== Schématique ===


Ligne 355 : Ligne 1 076 :


=== Fichier kicad ===
=== Fichier kicad ===
[[Fichier:2024-PSE-G2-Prog.zip|centré]]
[[Fichier:2024-PSE-G2-Prog VFinale sans erreur.zip|alt=2024-PSE-G2-Prog VFinale|centré]]
<p style="clear: both;" />
<p style="clear: both;" />


Ligne 366 : Ligne 1 087 :


==== Test leds et boutons ====
==== Test leds et boutons ====
Afin de vérifier que notre carte fonctionne correctement après brasure, on code un programme permettant d'allumer une led périodiquement puis un autre programme allumant une led lorsqu'un bouton poussoir est pressé.  
Afin de vérifier que notre carte fonctionne correctement après sa brasure, on code un programme permettant d'allumer une LED lorsqu'un bouton poussoir est pressé. Chaque bouton est associé à une LED.
 
===== Modification de l'horloge =====
<syntaxhighlight lang="c" line="1">
 
void setupClock(void)
{
CLKSEL0 = 0b00010101;  // sélection de l'horloge externe
CLKSEL1 = 0b00001111;  // minimum de 8Mhz
CLKPR = 0b10000000;    // modification du diviseur d'horloge (CLKPCE=1)
CLKPR = 0;              // 0 pour pas de diviseur (diviseur de 1)
}
 
</syntaxhighlight>
 
====== Fonction initialisation  des pins ======
<syntaxhighlight lang="c" line="1">
void setupPin(volatile uint8_t* PORTx, volatile uint8_t* DDRx, uint8_t pin, pinmode mode) {
    switch (mode)
    {
    case INPUT: // Forcage pin à 0
        *DDRx &= ~(1 << pin);
        break;
    case INPUT_PULL_UP: // Forcage pin à 0
        *DDRx &= ~(1 << pin);
        *PORTx |= (1 << pin);
        break;
    case OUTPUT: // Forcage pin à 1
        *DDRx |= (1 << pin);
        break;
    }
}
</syntaxhighlight>
 
====== Fonction initialisation de l'ensemble des boutons et LEDs ======
<syntaxhighlight lang="c" line="1">
void setupHardware() {
    setupClock();
 
    setupPin(LEDs_PORT, LEDs_DDR, LED1_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED2_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED3_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED4_PIN, OUTPUT);
 
    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Up_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Down_PIN, INPUT_PULL_UP);
    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Left_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Right_PIN, INPUT_PULL_UP);
}
 
</syntaxhighlight>
 
====== Fonction de lecture de pin ======
<syntaxhighlight lang="c" line="1">
int readPin(volatile uint8_t* PINx, uint8_t pin) {
    return (*PINx & (1 << pin));
}
</syntaxhighlight>
 
====== Fonction d'écriture de pin ======
<syntaxhighlight lang="c" line="1">
void writePin(volatile uint8_t* PORTx, uint8_t pin, write level) {
    if (level == LOW)
        *PORTx |= (1 << pin);
    else
        *PORTx &= ~(1 << pin);
}
</syntaxhighlight>
 
====== Fonction pour initialiser les composantes de la cartes ======
<syntaxhighlight lang="c" line="1">
// ------------------ Boutons ------------------ //
#define BTNsUp_Left_PORT &PORTC
#define BTNsUp_Left_DDR &DDRC
#define BTNsUp_Left_PIN &PINC
 
#define BTNsDown_Right_PORT &PORTB
#define BTNsDown_Right_DDR &DDRB
#define BTNsDown_Right_PIN &PINB
 
#define BTN_Up_PIN PC4
#define BTN_Down_PIN PB5
#define BTN_Left_PIN PC6
#define BTN_Right_PIN PB6
 
// ------------------ LEDs ------------------ //
#define LEDs_PORT &PORTD
#define LEDs_DDR &DDRD
#define LEDs_PIN &PIND
 
#define LED1_PIN PD0
#define LED2_PIN PD1
#define LED3_PIN PD2
#define LED4_PIN PD3
 
// ------------------ Enum ------------------ //
typedef enum {
    INPUT,
    INPUT_PULL_UP,
    OUTPUT,
} pinmode;
 
 
void setupHardware() {
    setupClock();
 
    setupPin(LEDs_PORT, LEDs_DDR, LED1_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED2_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED3_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED4_PIN, OUTPUT);
 
    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Up_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Down_PIN, INPUT_PULL_UP);
    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Left_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Right_PIN, INPUT_PULL_UP);
}
 
</syntaxhighlight>
 
==== LUFA ====
Afin de pouvoir faire de notre carte un périphérique USB, nous allons utiliser la LUFA (Lightweight USB Framefork for AVRs).
 
* Tout d'abord, nous avons codé un programme permettant de voir si la lufa détecte bien les boutons-poussoirs de notre carte comme des points d'accès d'entrées en affichant sur le minicom quel bouton-poussoir était pressé par l'utilisateur. Notre code se trouve dans le répertoire se ci-dessous :  [https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/01%20-%20Programmateur%20AVR/02%20-%20Programmation/lufa-LUFA-210130-NSI/se/VirtualSerial https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/01%20-%20Programmateur%20AVR/programmation/lufa-LUFA-210130-NSI/se/VirtualSerial]
Pour écrire notre code, nous avons réutilisé l'exemple VirtualSerial récupéré au repertoire suivant : <code>LUFA/Demos/Device/LowLevel/VirtualSerial</code>
 
 
Le code exemple du VirtualSerial permet d'écrire des données à notre pc (visible via minicom).
 
 


Afin de mettre le programme sur notre carte, on vérifie au préalable que notre carte est bien reconnue en tant que périphérique USB à l'aide de la commande lsusb.
Afin de mettre le programme sur notre carte, on vérifie au préalable que notre carte est bien reconnue en tant que périphérique USB à l'aide de la commande lsusb.
Ligne 374 : Ligne 1 223 :
<p style="clear: both;" />
<p style="clear: both;" />


Pour plus de détail sur notre périphérique usb branché on doit faire la commande " ''lsusb -s [bus]:[device] -v'' " :<syntaxhighlight lang="terminfo" line="1">
Pour plus de détail sur notre périphérique usb branché on doit faire la commande suivante :<syntaxhighlight lang="terminfo" line="1">
cedricagathe@computer:~$ lsusb -s 003:008 -v
cedricagathe@computer:~$ lsusb -s 003:008 -v


Ligne 422 : Ligne 1 271 :
</syntaxhighlight>
</syntaxhighlight>


Comme vu en cours, on revoit nos descripteurs ainsi que la description des interfaces de l'usb ainsi que d'autres données.Une fois notre carte détectée, on appuie sur le bouton HWB puis reset afin de mettre notre carte en mode DFU. On téléverse ensuite notre programme sur la carte à l'aide d'un makefile préalablement codé en tapant la commande make upload.
Comme vu en cours, on revoit nos descripteurs ainsi que la description des interfaces de l'usb ainsi que d'autres données. Une fois notre carte détectée, on appuie en continue sur le bouton HWB puis une seule impulsion sur le bouton RESET afin de mettre notre carte en mode DFU et ensuite on relache HWB.  
 
On pourra alors téléverser notre programme sur la carte à l'aide d'un makefile préalablement codé en tapant la commande <code>make dfu</code>.
<p style="clear: both;" />
<p style="clear: both;" />


Ligne 429 : Ligne 1 280 :
<p style="clear: both;" />
<p style="clear: both;" />


==== LUFA ====
* On place le programme dans notre carte à l'aide de la commande <code>make upload</code>. Pour ce faire, il faut modifier le makefile original par ceci :
Afin de pouvoir faire de notre carte un périphérique USB, nous allons utiliser la LUFA (Lightweight USB Framefork for AVRs).
 
* Tout d'abord, nous avons codé un programme permettant de voir si la lufa détecte bien les boutons-poussoirs de notre carte comme des points d'accès d'entrées en affichant sur le minicom quel bouton-poussoir était pressé par l'utilisateur. Notre code se trouve dans le répertoire se ci-dessous :  https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/01%20-%20Programmateur%20AVR/programmation/lufa-LUFA-210130-NSI/se/VirtualSerial 
Pour écrire notre code, nous avons réutilisé l'exemple VirtualSerial récupéré au repertoire suivant :
 
LUFA/Demos/Device/LowLevel/VirtualSerial
 
* On place le programme dans notre carte à l'aide de la commande make upload. Pour ce faire, il faut modifier le makefile original par ceci :
<syntaxhighlight lang="makefile" line="1" start="1">
<syntaxhighlight lang="makefile" line="1" start="1">
MCU          = atmega8u2
MCU          = atmega8u2
Ligne 452 : Ligne 1 295 :
PROGRAMMER  = dfu-programmer
PROGRAMMER  = dfu-programmer


all:
# Include LUFA-specific DMBS extension modules
DMBS_LUFA_PATH ?= $(LUFA_PATH)/Build/LUFA
include $(DMBS_LUFA_PATH)/lufa-sources.mk
include $(DMBS_LUFA_PATH)/lufa-gcc.mk


upload: $(TARGET).hex
# Include common DMBS build system modules
$(PROGRAMMER) $(MCU) erase
DMBS_PATH      ?= $(LUFA_PATH)/Build/DMBS/DMBS
$(PROGRAMMER) $(MCU) flash $(TARGET).hex
include $(DMBS_PATH)/core.mk
$(PROGRAMMER) $(MCU) reset
include $(DMBS_PATH)/cppcheck.mk
include $(DMBS_PATH)/doxygen.mk
include $(DMBS_PATH)/dfu.mk
include $(DMBS_PATH)/gcc.mk
include $(DMBS_PATH)/hid.mk
include $(DMBS_PATH)/avrdude.mk
include $(DMBS_PATH)/atprogram.mk


clean:
rm -f *.bin *.elf *.lss *.map *.sym *.eep
</syntaxhighlight>
</syntaxhighlight>


Ligne 469 : Ligne 1 319 :
sudo minicom -os
sudo minicom -os
</syntaxhighlight>
</syntaxhighlight>
<p style="clear: both;" />
<p style="clear: both;" />Ensuite nous entrons dans Serial port setup :[[Fichier:Image terminal apres commande minicom -os.png|gauche|sans_cadre|361x361px]]
 
Ensuite nous entrons dans Serial port setup :
[[Fichier:Image terminal apres commande minicom -os.png|gauche|sans_cadre|361x361px]]
[[Fichier:Menu serial port ssetup minicom.png|centré|sans_cadre|395x395px]]
[[Fichier:Menu serial port ssetup minicom.png|centré|sans_cadre|395x395px]]


Ligne 497 : Ligne 1 344 :
<p style="clear: both;" /><p style="clear: both;" />
<p style="clear: both;" /><p style="clear: both;" />


===== Modification du fichier virtual.c =====
===== <big>Modification du fichier virtual.c</big> =====
 
====== Afficher sur le minicom lorsqu'un bouton est pressé et test leds ======
<syntaxhighlight lang="c" line="1">
void CDC_Task(void) {
static char ReportBuffer[64]; // Buffer pour stocker le message à envoyer
static bool ActionSent = false; // Pour éviter d'envoyer plusieurs fois le même message


====== Modification de l'horloge ======
bool hasMessage = false;  // Indique si un bouton a été pressé
Ainsi notre microprocesseur détecte notre quartz comme étant du 16MHz<syntaxhighlight lang="c" line="1">


void SetupHardware(void)
// Vérifie que l’appareil est connecté et configuré
{
if (USB_DeviceState != DEVICE_STATE_Configured)
CLKSEL0 = 0b00010101;   // sélection de l'horloge externe
return;
CLKSEL1 = 0b00001111;   // minimum de 8Mhz
 
CLKPR = 0b10000000;    // modification du diviseur d'horloge (CLKPCE=1)
// Détection du bouton haut
CLKPR = 0;             // 0 pour pas de diviseur (diviseur de 1)
if (!readPin_HardwareProgAVR(BTNsUp_Left_PIN, BTN_Up_PIN)) {
hasMessage = true;
strcpy(ReportBuffer, "bouton du haut\r\n");
 
toggleLed(LEDs_PORT, LEDs_PIN, LED1_PIN);
_delay_ms(300); // Anti-rebond
}
else ActionSent = false;
 
 
// Détection du bouton bas
if (!readPin_HardwareProgAVR(BTNsDown_Right_PIN, BTN_Down_PIN)) {
hasMessage = true;
strcpy(ReportBuffer, "bouton du bas\r\n");
 
toggleLed(LEDs_PORT, LEDs_PIN, LED2_PIN);
 
_delay_ms(300);
}
else ActionSent = false;
 
// Détection du bouton gauche
if (!readPin_HardwareProgAVR(BTNsUp_Left_PIN, BTN_Left_PIN)) {
hasMessage = true;
strcpy(ReportBuffer, "bouton de gauche\r\n");
 
toggleLed(LEDs_PORT, LEDs_PIN, LED3_PIN);
_delay_ms(300);
 
}
else ActionSent = false;
 
// Détection du bouton droit
if (!readPin_HardwareProgAVR(BTNsDown_Right_PIN, BTN_Right_PIN)) {
hasMessage = true;
ReportString = "bouton de droite\r\n";
// spi_test_octet(ReportBuffer);
// getIDspi(ReportBuffer);
 
toggleLed(LEDs_PORT, LEDs_PIN, LED4_PIN);
_delay_ms(300);
}
else ActionSent = false;
 
// Si un bouton a été pressé et qu'aucune action n’a encore été envoyée
if (hasMessage && (ActionSent == false) && LineEncoding.BaudRateBPS) {
ActionSent = true;
 
// Envoie le message via USB
Endpoint_SelectEndpoint(CDC_TX_EPADDR);
Endpoint_Write_Stream_LE(ReportBuffer, strlen(ReportBuffer), NULL);
 
bool IsFull = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);
Endpoint_ClearIN();
 
if (IsFull) {
Endpoint_WaitUntilReady();
 
Endpoint_ClearIN();
}
}
 
// Nettoie le buffer de réception (inutile ici mais bonne pratique)
Endpoint_SelectEndpoint(CDC_RX_EPADDR);
if (Endpoint_IsOUTReceived())
Endpoint_ClearOUT();
}
</syntaxhighlight>
<p style="clear: both;" />L’ajout d’un délai de 300 ms est nécessaire pour éviter les rebonds des boutons mécaniques. Sans cela, une pression peut être détectée plusieurs fois à cause des oscillations électriques rapides à l’activation.<p style="clear: both;" />Ici on voit que le code peut être factorisé. Cela n'a pas été fais pour tester individuellement des fonctions différentes sur chaque bouton (cf prochaine section).
 
====== '''Récupération ID microcontroleur''' ======
<p style="clear: both;" />Il y a des tentatives afin de récuprer un identifiant d'un microcontroleur via la fonction getIDspi(ReportBuffer). <syntaxhighlight lang="c">
#include "../spi.h"
 
#include <avr/io.h>
#include <string.h>
#include <util/delay.h>
 
 
#define SPI_DDR        DDRB
#define SPI_PORT        PORTB
#define SPI_SS          PB0
#define SPI_SCK        PB1
#define SPI_MOSI        PB2
#define SPI_MISO        PB3
 
void spi_init(void) {                                // Initialisation du bus SPI
    SPI_DDR |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS);  // Définition des sorties
    SPI_DDR &= ~(1 << SPI_MISO);                           // Définition de l'entrée
     SPI_PORT |= (1 << SPI_SS);                            // Désactivation du périphérique
    SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0);      // Activation SPI (SPE) en état maître (MSTR)
    SPSR &= ~(1 << SPI2X);                                // horloge F_CPU/128 (SPI2X=0, SPR1=1,SPR0=1)
}
 
void spi_activer(void) {                              // Activer le périphérique
    SPI_PORT &= ~(1 << SPI_SS);                           // Ligne SS à l'état bas
}
 
void spi_desactiver(void) {                          // Désactiver le périphérique
    SPI_PORT |= (1 << SPI_SS);                            // Ligne SS à l'état haut
}
}


</syntaxhighlight>
uint8_t spi_echange(uint8_t envoi) {    // Communication sur le bus SPI
    SPDR = envoi;                                                  // Octet a envoyer
    while (!(SPSR & (1 << SPIF)));                                    // Attente fin envoi (drapeau SPIF du statut)
    return SPDR;                                                    // Octet reçu
}


====== Fonction pour les boutons ======
uint8_t spi_transaction(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
<syntaxhighlight lang="c" line="1">
    spi_echange(a);
void init_boutons(void){
    spi_echange(b);
// A MODIF CEDRIC
    spi_echange(c);
//bouton sw3 du haut
     return spi_echange(d);
DDRB = 0x00; //0 input
DDRC = 0x00;
PORTB = 0x60; //PB5 -> SW6 (bas) et PB6 -> SW4 (droite)
     PORTC = 0x50; //PC4 -> SW3 (haut) et PC6 -> SW5 (gauche)
MCUCR |= 0x10; //Pour activer resistance cf DS p38
}
}


</syntaxhighlight>
void end_pmode(void) {
    PORTB &= ~(1 << PB0);
}


====== Fonction pour la led ======
#define nibble2char(n)  (((n)<10)?'0'+(n):'a'+(n)-10)
<syntaxhighlight lang="c" line="1">


void init_leds(void){
void convert(unsigned char byte, char* string) {
// A MODIF CEDRIC
    string[0] = nibble2char(byte >> 4);
DDRD |= 0x0f; //PD3-0 : leds 1 à 4
    string[1] = nibble2char(byte & 0x0f);
    string[2] = '\0';
}
}


</syntaxhighlight>
void getIDspi(char* ReportString) {
    spi_activer();
    _delay_ms(10); // délai optionnel pour stabilité
 
    uint8_t high = spi_transaction(0x30, 0x00, 0x00, 0x00); //cf p8 Device Code de la DS AVR_ISP
    spi_desactiver();
    spi_activer();
    _delay_ms(10); // délai optionnel pour stabilité
    // convert(high, format + 5);  // "xx" remplacé par la valeur convertie
 
    uint8_t middle = spi_transaction(0x30, 0x00, 0x01, 0x00);
    spi_desactiver();
    spi_activer();
    _delay_ms(10); // délai optionnel pour stabilité
    // convert(middle, format + 14);  // "yy" remplacé par middle
    uint8_t low = spi_transaction(0x30, 0x00, 0x02, 0x00);
    spi_desactiver();
    // convert(low, format + 22);    // "zz" remplacé par low
 
 
 
    char highHex[3], middleHex[3], lowHex[3];
    convert(high, highHex);
    convert(middle, middleHex);
    convert(low, lowHex);
 
    // Construire la chaîne manuellement
    char* ptr = ReportString;
    const char* prefix = "RAW SPI ID: H=0x";
    while (*prefix) *ptr++ = *prefix++;
 
    *ptr++ = highHex[0];
    *ptr++ = highHex[1];
 
    const char* midStr = " M=0x";
    while (*midStr) *ptr++ = *midStr++;
 
    *ptr++ = middleHex[0];
    *ptr++ = middleHex[1];
 
    const char* lowStr = " L=0x";
    while (*lowStr) *ptr++ = *lowStr++;


====== Afficher sur le minicom lorsqu'un bouton est pressé ======
    *ptr++ = lowHex[0];
<syntaxhighlight lang="c" line="1">
    *ptr++ = lowHex[1];


    *ptr++ = '\r';
    *ptr++ = '\n';
    *ptr = '\0';
}
</syntaxhighlight><p style="clear: both;" />Avec l’aide de M. Redon, nous avons tenté de récupérer l’identifiant unique d’un microcontrôleur AVR en utilisant le protocole SPI. Pour cela, nous avons développé un programme basé sur une communication SPI bas-niveau, avec les fonctions d’initialisation, d’activation/désactivation du bus, et d’échange de données via SPI.<p style="clear: both;" />Le principe était d’envoyer des commandes spécifiques conformes au protocole ISP (In-System Programming) d’Atmel, notamment l’envoi de la commande <code>0x30</code> suivie d’adresses pour lire les octets composant l’ID (haut, milieu, bas). La fonction <code>getIDspi()</code> réalise ces transactions successives et convertit les valeurs reçues en chaîne hexadécimale lisible.<p style="clear: both;" /><p style="clear: both;" />Nous avons également implémenté une fonction de test simple, <code>spi_test_octet()</code>, qui envoie un octet <code>0x55</code> et devrait recevoir la même valeur en retour si le périphérique répond correctement.<p style="clear: both;" /><syntaxhighlight lang="c">
void spi_test_octet(char* ReportString) {
    // Configuration de RESET en sortie
    SPI_DDR |= (1 << SPI_SS);          // RESET = PB0 en sortie
    SPI_PORT &= ~(1 << SPI_SS);        // RESET à 0 : mode programmation
    // _delay_ms(30);                    // Attente suffisante (20ms minimum)


void CDC_Task(void)
    // Envoie de la commande ISP pour vérifier si la cible répond
{
    // spi_activer();                    // SS SPI actif (LOW)
        char*      ReportString    = NULL;
    // uint8_t check = spi_transaction(0xAC, 0x53, 0x00, 0x00); // Activation ISP
  static bool ActionSent      = false;
    // spi_desactiver();                 // SS SPI inactif (HIGH)


init_boutons();
    // if (check != 0x53) {
init_leds();
    //    strcpy(ReportString, "Erreur: mode ISP non actif\r\n");
    //    SPI_PORT |= (1 << SPI_SS);     // RESET à 1 : fin prog
    //    return;
    // }


/* Device must be connected and configured for the task to run */
    // Si la cible est bien en mode programmation, test SPI
if (USB_DeviceState != DEVICE_STATE_Configured)
    spi_activer();
  return;
    uint8_t response = spi_echange(0x55);
    spi_desactiver();


  /* Determine if a button action has occurred */
    SPI_PORT |= (1 << SPI_SS); // Fin du mode ISP (RESET à 1)
if ((PINC&0x10)<=0){ //PC4
  ReportString = "bouton du haut\r\n"; //affiché dans le minicom
  PORTD = 0x01; //allume led1
  _delay_ms(300);
}
if ((PINB&0x20)<=0){ //PB5
    ReportString = "bouton du bas\r\n";
PORTD = 0x02; //allume led2
_delay_ms(300);
}
if ((PINC&0x40)<=0){ //PC6
    ReportString = "bouton de gauche\r\n";
PORTD = 0x04; //allume led3
_delay_ms(300);
}
if ((PINB&0x40)<=0){ //PB6
    ReportString = "bouton de droite\r\n";
PORTD = 0x08; //allume led4
_delay_ms(300);
}
else
  ActionSent = false;


getID(ReportString);
    // Construction de la chaîne
    char hexStr[3];
    convert(response, hexStr);


[...]
    char* ptr = ReportString;
    const char* prefix = "SPI test response: 0x";
    while (*prefix) *ptr++ = *prefix++;
    *ptr++ = hexStr[0];
    *ptr++ = hexStr[1];
    *ptr++ = '\r';
    *ptr++ = '\n';
    *ptr = '\0';
}
}
</syntaxhighlight>
</syntaxhighlight>'''Cependant, malgré plusieurs essais, la récupération stable de l’ID n’a pas été réussie :'''
<p style="clear: both;" />
* Le signal SPI semble fonctionner de manière intermittente.
* La réponse attendue (<code>0x55</code>) est parfois reçue, mais de façon instable.
* Cela pourrait venir d’un problème matériel (câblage, niveau des signaux) ou d’un timing non respecté dans le protocole SPI/ISP.
 
* '''Fonctions SPI implémentées''' :
** <code>spi_init()</code> : configure les broches et paramètres SPI maître.
** <code>spi_activer()</code> / <code>spi_desactiver()</code> : contrôle de la ligne SS (Slave Select).
** <code>spi_echange()</code> : envoie et réception d’un octet SPI.
** <code>spi_transaction()</code> : envoie une séquence de quatre octets en SPI, utile pour la commande ISP.
 
 
Jusqu'ici, je n'ai pas de piste pour pouvoir continuer, sachant que le câblage n'est pas le problème.

Version actuelle datée du 16 juin 2025 à 15:17

Lien GIT

Voici le lien git pour accéder aux différents fichiers relatifs à notre projet : https://gitea.plil.fr/ahouduss/se3_2024_B2.git

Description du projet

Objectif

L'objectif de ce projet est de concevoir une station domotique capable de collecter et d'afficher des mesures provenant de capteurs. Elle devra également être capable d'activer des actionneurs, tels que des LEDs, des cadenas ou tout autre dispositif, en fonction des besoins.

Cahier des charges

La station domotique devra permettre l'affichage des informations suivantes concernant une pièce :

  • Température ambiante ;
  • Taux d'humidité ;
  • Présence humaine (via capteur de mouvement) ;
  • D'autres paramètres pourront être ajoutés en fonction de l'avancement du projet.

Elle devra aussi permettre de contrôler différents actionneurs dans la pièce, tels que :

  • L'éclairage, en fonction de la présence d'une personne (via un capteur de mouvement) ;
  • D'autres dispositifs pourront être intégrés en fonction des besoins.

Des capteurs et actionneurs supplémentaires pourront être ajoutés si le projet atteint ses objectifs initiaux.

Spécification techniques

Microcontrôleur

Le projet nécessite un microcontrôleur, qui contiendra le programme, et qui communiquera avec les autres composants via les GPIOs.

Nous avons le choix entre plusieurs modèles de microcontrôleur : ATmega16u4, AT90USB1286, AT90USB1287.

Voici un tableau comparatif afin de sélectionner le plus adapté pour notre usage :

Caractéristiques ATmega16U4 AT90USB1286 AT90USB1287
Architecture AVR 8 bits AVR 8 bits AVR 8 bits
Mémoire Flash 16 KB 128 KB 128 KB
RAM (SRAM) 1.25 KB 4 KB 4 KB
EEPROM 512 Bytes 4 KB 4 KB
Fréquence d'horloge max. 16 MHz 16 MHz 16 MHz
Nombre de broches GPIO 26 48 48
Interfaces de communication UART, SPI, I²C, USB 2.0 UART, SPI, I²C, USB 2.0 UART, SPI, I²C, USB 2.0
Contrôleur USB intégré Oui (USB 2.0) Oui (USB 2.0) Oui (USB 2.0)
Taille des registres 8 bits 8 bits 8 bits
Nombre de broches 32 64 64
Différences principales Conçu pour des applications compactes avec

moins de mémoire et d'E/S

Plus de mémoire, adapté à des projets complexes nécessitant de nombreuses E/S et de la mémoire Similaire au AT90USB1286 mais avec des fonctionnalités spécifiques

pour certaines configurations USB (e.g., modes host/OTG).

Lien documentation https://www.microchip.com/en-us/product/atmega16u4 https://www.microchip.com/en-us/product/at90usb1286 https://www.microchip.com/en-us/product/at90usb1287

Avec ce tableau, on constate que l'ATmega16U4 ne possède pas suffisamment de broches GPIOs. Cependant l'AT90USB1286 et son homologue l'AT90USB1287 dépassent notre cadre d'usage (utilisation mode USB spécifique HOST/OTG, etc... ).

Le compromis est donc d'opter pour un ATmega32u4 afin d'avoir suffisamment de broches et de mémoire.

Caractéristiques ATmega32U4
Architecture AVR 8 bits
Mémoire Flash 32 KB
RAM (SRAM) 2.5 KB
EEPROM 1 KB
Fréquence d'horloge max. 16 MHz
Nombre de broches GPIO 26
Interfaces de communication UART, SPI, I²C, USB 2.0
Contrôleur USB intégré Oui (USB 2.0)
Taille des registres 8 bits
Nombre de broches 32
Différences principales Conçu pour des applications nécessitant un contrôleur USB intégré, avec une mémoire et un nombre de broches intermédiaires

Datasheet ATmega32u4 :

Datasheet du microcontroleur : ATMEGA32U4
AVR Hardware Design Considerations


Communication

La station utilisera une puce NRF24L01 pour la communication sans fil entre les différents actionneurs et capteurs.

La communication entre le pc et la station se fera quant à elle en USB.

Lien tutoriel utilisation de puces à distance : NRF24L01

Datasheet NRF24L01 :

Datasheet module de communication : NRF24L01

Énergie

La station sera alimentée de manière hybride, selon les scénarios suivants  : - Par un port USB, pour la programmation, les tests et la configuration avec affichage sur moniteur PC ;

- Par une batterie Lithium, en mode autonome pour une utilisation prolongée (avec affichage écran LCD dans un second temps).

Les capteurs/actionneurs seront alimentées de manière hybride, selon les scénarios suivants :

- Par un port USB, pour la programmation, les tests et la configuration ;

- Par une batterie Lithium, en mode autonome pour son usage définitif.

Modèles de batterie à notre disposition :

  • Batterie 3.7V 100 mAh, connecteur molex mâle ;
  • Batterie 3.7V 300 mAh, connecteur molex mâle ;


Nous allons ajouter la possibilité de recharger notre batterie depuis notre carte via le composant MAX1811. La carte se rechargera lorsqu'elle sera branché en USB mais l'USB fournit du 5V, ce qui peut nuire à certains de nos composants...

Pour proteger les composants, nous allons ajouter un régulateur de tension pour garder une tension de 3,3V sur l'ensemble de notre carte.


Datasheets du chargeur et du régulateur :

Datasheet du chargeur : MAX1811
Datasheet du régulateur : LTC3531

Affichage

Dans un premier temps, les informations seront remontées via la connexion USB à un programme sur PC (selon les exigences du cahier des charges).

Dans un second temps, un écran LCD sera utilisé pour afficher les données directement sur la station, offrant ainsi une solution autonome, sous réserve du temps disponible pour cette implémentation.

Datasheet de l'écran graphique utilisé :

Datasheet de l'écran : NHD‐C12832A1Z‐FSW‐FBW‐3V3

On décide de programmer l'écran en C. On code donc notre écran via l'API "glcd.h". L'écran sera composé d'un menu permettant de naviguer parmi les différents capteurs enregistrés afin de consulter la valeur renvoyée par le capteur choisi. Les boutons intégrés sur la carte ainsi que l'encodeur rotatif permettront à l'utilisateur de naviguer entre les différents capteurs.

Diverses

La carte comportera également une led afin d'indiquer son état d'alimentation ainsi que deux autres leds permettant de faire des tests de programmation.

Hardware

Schématique

Notre schéma électrique

Schéma électrique V1 KICAD

Comprendre notre schéma

Comprendre la schématique

Vue 3D

Carte station en 3D - Vue arrière
Carte station en 3D - Vue avant


Brasure

Software

Capteurs

Capteur de mouvement - HC-SR501

Principe physique

Le capteur de mouvement HC-SR501 est un capteur infrarouge passif (PIR), ce qui signifie qu’il ne produit aucun rayonnement mais détecte celui émis naturellement par les objets chauds, notamment le corps humain.

Ces deux cellules pyroélectriques sont disposées de manière à percevoir deux zones distinctes du champ de vision. En l'absence de mouvement, les deux reçoivent une quantité similaire d'infrarouge, et le signal reste équilibré.

Lorsqu'un corps chaud passe devant le capteur, la quantité d’infrarouge captée change entre les deux cellules, créant un déséquilibre. Ce changement est interprété comme un mouvement.


Un dôme en plastique blanc recouvre le capteur : c’est une lentille de Fresnel.

Lentille Fresnel du capteur de mouvement

Elle concentre et divise la lumière infrarouge en plusieurs zones, augmentant ainsi la portée et la sensibilité du capteur en "segmentant" son champ de vision. Ainsi, même un petit mouvement crée une variation significative de rayonnement perçu.

Spécifications techniques

On utilise un capteur de mouvement HC-SR501 (voir datasheet ci-dessous) afin de détecter ou non la présence de quelqu'un dans une pièce. L'intérêt est de pouvoir ensuite allumer la lumière si une personne est présente ou a contrario l'éteindre si la pièce est vide. Ici la lumière sera modélisée par une led présente sur l'arduino UNO en guise de démonstration.

datasheet_mvmt
datasheet_mvmt
mvmt
capteur_mvmt

  • Jumper Set

Les deux modes, repeat ou single trigger, permettent de régler le trigger de schmith permettant la détection d'une présence. En single, on effectue un seul trigger afin de détecter spontanément une présence (ex : Cas alarme intrusion). Dans l'autre mode on souhaite détecter un mouvement peu importe si celui-ci est déjà détecter. Par exemple, besoin d'un mouvement après un certains nombre de temps pour que la led reste allumée ou bien besoin de réactiver de temps à autre le capteur en bougeant.

  • Sensitivty Adjust

On modifie le potentiomètre à l'aide d'un tournevis afin d'ajuster la sensibilité à la présence de notre main, par exemple pour l'amplitude de nos mouvements.

  • Time Delay Adjust

Ici le potentiomètre permet d'ajuster le temps entre deux seuils de détection afin d'éviter la détection après des mouvements parasites, par exemple pour déclencher sans erreur une alarme intrusion.

Circuit

On connecte le GND à la masse de l'arduino, le power au 5V et la sortie au pin PB0 (digital 8). Voir la vidéo de démonstration pour plus de détails.

Programmation

Voici le code afin d'allumer une led dès qu'une présence est détectée.

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    DDRB |= (1 << PB5); //led D13 en sortie
    while (1) {
        if (PINB & (1 << PB0)) { //si mvmt
            PORTB |= (1 << PB5);  //led allumée
        } else { //si absence
            PORTB &= ~(1 << PB5); //led éteinte
        }
        _delay_ms(500); // Peut être baisser ou augmenter pour regler la sensibilité de détection
    }
    return 0;
}

Démonstration

Voici le résultat en vidéo ci-dessous. On constate bien que la led s'allume lorsqu'une présence est détectée.

Dans un second temps, on fera communiquer ce capteur avec notre carte station domotique par le biais des modules de communication radio (NRF24L01) afin d'afficher l'état de la pièce sur l'écran et également transmettre ces données via le port série.

Capteur de température - DS18B20

Principe physique

Ce capteur fonctionne grâce à un principe physique appelé variation de la résistance électrique avec la température. À l’intérieur du capteur, il y a un composant semi-conducteur, souvent une diode ou une jonction PN, qui change son comportement électrique selon la température.

Quand la température augmente, la façon dont les électrons se déplacent dans ce matériau change, ce qui modifie la tension ou le courant électrique mesuré.

Un convertisseur analogique-numérique (CAN) est intégré au circuit afin de transferer par la suite la mesure via un protocole 1-Wire.

Spécifications techniques

On utilise aussi le capteur de température DS18B20 (voir datasheet ci-dessous) afin de mesurer la température dans une matière tel que l'eau ou bien la terre (pour une piscine ou une plante par exemple).

Datasheet_temp_eau
Datasheet_temp_eau

Circuit

On branche les 3 broches de notre sonde de la manière suivante :

  • le GND est relié à la masse
  • le power est relié au 3,3V (peut également être relié sur le +5V si besoin)
  • le fil de données est branché sur un pin digital (valeur TOR) et ici sur PD2 (digital 2).

On doit brancher une résistance de 4,7kΩ entre le fil de données et le 3,3V (ou +5V).

capteur_eau
capteur_eau

Programmation
Arduino

Pour commencer, on a testé si notre sonde fonctionnait correctement à l'aide d'un code arduino et on a constaté aucun souci (Code Arduino).

C

Dans le cadre de ce projet, nous sommes partis d’un code initialement fonctionnel développé sur Arduino, puis nous l’avons adapté à notre propre environnement en langage C, plus proche du matériel et sans dépendance aux bibliothèques Arduino. Pour cela, nous avons fusionné deux ressources trouvées sur GitHub afin d'obtenir une base de code cohérente, fonctionnelle et surtout structurée de manière à répondre à nos besoins techniques.

Nous avons utilisé les fichiers disponibles dans le répertoire suivant : Code C

Protocole OneWire

Le protocole OneWire est un protocole de communication série développé par Maxim Integrated. Il permet à un microcontrôleur de communiquer avec un ou plusieurs périphériques (comme des capteurs de température, EEPROM, etc.) via un seul fil de données (en plus du GND). Ce fil est bidirectionnel et transporte à la fois les données et l’horloge synchronisée par le maître (généralement le microcontrôleur).

Ce protocole est particulièrement utilisé avec des capteurs qui mesurent la température et la transmettent sous forme numérique. Le principal avantage du OneWire est sa simplicité matérielle : un seul fil suffit pour communiquer avec plusieurs périphériques, chacun ayant une adresse unique codée en ROM.

Ce lien/ est un tutoriel qui nous explique comment fonctionne le protocole OneWire et sur ce lien on retrouve un exemple de code complet mais pour notre usage nous nous sommes limités aux fonctionnalités essentielles ( à savoir écriture et lecture pour un unique appareil connecté).

onewire.h, onewire.c : pour remplacer la librairie OneWire.h de l'arduino afin de communiquer avec l'unique fil de données de la sonde en pure C.

  • onewireInit : reset le bus de données et renvoie une erreur si le capteur de répond pas.
  • onewireWriteBit : envoie un bit sur le bus de données en respectant le temps d'envoi du protocole Onewire.
  • onewireWrite : transmet un octet en utilisant la fonction précédente.
  • onewireReadbit : lit un bit sur le bus de données.
  • onewireRead : lit un octet sur le bus de données.

Code de notre sonde de température Nous avons étudier le fonctionnement de notre sonde de temperature via sa datasheet. Nous nous sommes aidés de celle ci et de son code équivalent Arduino afin de pouvoir programmer une librairie en C (le fichier .c et le fichier .h). ds18b20.h, ds18b20.c : pour les fonctions principales utiles à la communication entre notre sonde et notre microcontroleur.

  • ds18B20crc8 : CRC signifie cyclic redundacy check est l'octet renvoyé par cette fonction qui permet de savoir si la transmission s'est effectuée sans erreurs.
  • ds18b20match : utile si il y a plusieurs capteurs (pas le cas ici).
  • ds18b20convert : la valeur de la température est stockée sur les deux premiers octets de la mémoire scratchpad. ds18b20convert permet de convertir ces octets en degré celsius.
  • ds18b20rsp : lit le scratchpad (mémoire temporaire) pour récupérer la valeur de la température (sur les deux premiers octets).
  • ds18b20wsp : écrit dans le scratchpad.
  • ds18b20csp  : copie les données du scratchpad dans l'eeprom du capteur.
  • ds18b20read : lit la température.
  • ds18b20rom : lit l'adresse du capteur rom (pas utile ici car un seul capteur).

    UART

Cette partie à été codé uniquement pour le debug car l'usage de l'UART sera négligé plus tard. Effectivement le but final c'est d'avoir un périphérique USB complet donc à coder via la LUFA. Le port série virtuel USB (CDC) créé par LUFA est reconnu par la plupart des OS modernes sans besoin de drivers spécifiques. On aura alors un projet modulaire !

UART.h, UART.c : pour afficher la température sur la liaison série. On définit deux fonctions :

  • USART_SendChar pour afficher un caractère sur le minicom.
  • USART_SendString pour afficher des mots sur le minicom. Rq : utiliser le retour chariot \r pour un affichage correct.

Main main.c : pour le code principal

#include <avr/io.h>
#include <util/delay.h>
#include <stdio.h>
#include "UART.h"
#include "ds18b20.h"

#define DS18B20_DDR   DDRD
#define DS18B20_PORT  PORTD
#define DS18B20_PIN   PIND
#define DS18B20_MASK  (1 << PD2)

int main(void)
{
    int16_t temperature_raw;
    char buffer[32];
    uint8_t error;

    USART_init(9600);
    USART_SendString("Debut lecture DS18B20...\r\n");

    while (1)
    {
        // Démarrer conversion
        error = ds18b20convert(&DS18B20_PORT, &DS18B20_DDR, &DS18B20_PIN, DS18B20_MASK, NULL);
        _delay_ms(800);  // attendre la fin de conversion

        if (error != DS18B20_ERROR_OK) {
            USART_SendString("Erreur conversion\r\n");
        }
        else {

            // Lire la température
            error = ds18b20read(&DS18B20_PORT, &DS18B20_DDR, &DS18B20_PIN, DS18B20_MASK, NULL, &temperature_raw);
            if (error == DS18B20_ERROR_OK) {
                float temperature_celsius = temperature_raw / 16.0;
                snprintf(buffer, sizeof(buffer), "Temp: %.2f C\r\n", temperature_celsius);
                USART_SendString(buffer);
            } else {
                snprintf(buffer, sizeof(buffer), "Erreur lecture: %d\r\n", error);
                USART_SendString(buffer);
            }
        }
        _delay_ms(200);
    }
    return 0;
}

Dans notre fonction main :

  • on initialise la liaison série.
  • on convertit les octets de la mémoire du capteur en une température en degré celsius.
  • on lit la température afin de l'afficher dans le minicom. Pour cela, il faut au préalable convertir notre température en flottant en des caractères avec une taille adaptée au buffer à l'aide de la fonction snprintf (string numbered print format).


Le DS18B20 mesure la température avec une résolution de 0,0625 °C, ce qui correspond à 1/16 de degré Celsius. Si le capteur renvoyait directement la température en °C sous forme entière, il serait impossible d’exprimer des fractions précises, comme 23,0625 °C.

En utilisant une valeur entière (int16_t) codant des fractions binaires, on peut :

  • Éviter les calculs en virgule flottante dans les systèmes embarqués (coûteux en ressources).
  • Avoir une grande précision avec un codage simple :

    1 bit de poids faible = 0,0625 °C → résolution sur 12 bits.

Makefile :

CC = avr-gcc
OBJCOPY = avr-objcopy
SIZE = avr-size

MCU = atmega328p
FCPU = 16000000UL

FLAGS = -mmcu=$(MCU) -Wl,-u,vfprintf -lprintf_flt -lm
CFLAGS = -Wall $(FLAGS) -DF_CPU=$(FCPU) -Os
LDFLAGS = $(FLAGS)

PROGRAMMER = avrdude
AVRDUDE_MCU = atmega328p
AVRDUDE_PORT = /dev/ttyACM0  # À adapter
AVRDUDE_BAUD = 115200
AVRDUDE_PROGRAMMER = arduino

TARGET = main
SOURCES = $(wildcard *.c)
OBJECTS = $(SOURCES:.c=.o)

all: $(TARGET).hex

clean:
	rm -f *.o $(TARGET).hex $(TARGET).elf eeprom.hex

$(TARGET).elf: $(OBJECTS)
	$(CC) -o $@ $^ $(LDFLAGS)

$(TARGET).hex: $(TARGET).elf
	$(OBJCOPY) -j .text -j .data -O ihex $< $@
	$(OBJCOPY) -j .eeprom --set-section-flags=.eeprom="alloc,load" \
		--change-section-lma .eeprom=0 -O ihex $< eeprom.hex

upload: $(TARGET).hex
	$(PROGRAMMER) -v -p $(AVRDUDE_MCU) -c $(AVRDUDE_PROGRAMMER) -P $(AVRDUDE_PORT) \
		-b $(AVRDUDE_BAUD) -D -U flash:w:$(TARGET).hex:i

size: $(TARGET).elf
	$(SIZE) --format=avr --mcu=$(MCU) $<
Démonstration

Voici le résultat en vidéo ci-dessous. On constate bien que la température affichée sur le minicom augmente lorsque la sonde reste longtemps dans la bouilloire chauffée au préalable.

Dans un second temps, on fera communiquer ce capteur avec notre carte station domotique par le biais des modules nrf24 afin d'afficher la température de la pièce sur l'écran.

Actionneur

Lumière

On peut décider d'allumer une lumière selon certains critères (par exemple lorsque le capteur de présence détecte quelqu'un). Ici allumer une led par exemple. Cette partie n'as pas encore été mise en place, faute de temps. Nous avons préférez avancer sur l'écran avant tout.

Ecran

Pour offrir une interface utilisateur intuitive, nous avons décidé d’afficher sur un écran les données issues des capteurs ainsi que l’état des actionneurs. Par exemple, il doit être possible de visualiser la température d’une pièce ou la détection de présence en temps réel.

Nous avons choisi d’utiliser un écran NHD‐C12832A1Z‐FSW‐FBW‐3V3 (stock de M. Boé). Initialement, nous avons tenté d’utiliser la bibliothèque graphique u8g2, réputée pour sa compatibilité avec de nombreux écrans. Cependant, malgré plusieurs essais de programmes issus de cette bibliothèque, l’écran restait vierge sans aucune information affichée.

Face à cette difficulté, nous avons décidé de simplifier notre approche en codant une instruction basique destinée à allumer l’ensemble des pixels de l’écran, en nous appuyant directement sur la datasheet du composant. Mais là encore, aucun résultat visible.

Cette absence de réaction nous a conduit à suspecter un problème matériel lié à la communication entre le microcontrôleur et l’écran. Nous avons alors découvert que notre écran ne fonctionne pas en I2C, contrairement à ce que nous avions initialement supposé, mais bien en SPI.

Pour corriger cela, nous avons modifié le câblage en coupant les pistes SDA et SCL (liées au bus I2C) puis connecté le pin SCL de l’écran au SCK du microcontrôleur, et le pin SI de l’écran au MOSI du microcontrôleur.

Modification de nos piste pour échnager avec l'écran


Malgré ces ajustements, l’écran restait toujours noir. Nous avons donc vérifié à l’oscilloscope la présence des signaux SPI transmis à l’écran, ce qui nous a confirmé que les données étaient bien envoyées.

signaux SCL et MOSI
signaux SCL et MOSI

Nous avons ensuite affiné notre code d’initialisation de l’écran, notamment en veillant à éteindre l’affichage pendant la configuration, puis à le rallumer une fois les paramètres correctement envoyés. Voici un extrait de la fonction d’initialisation :

void lcd_init() {
    lcd_reset();
    lcd_command(0xA0); // ADC select
    lcd_command(0xAE); // Display OFF
    lcd_command(0xC8); // COM direction scan
    lcd_command(0xA2); // LCD bias set
    lcd_command(0x2F); // Power Control set
    lcd_command(0x21); // Resistor Ratio Set
    lcd_command(0x81); // Electronic Volume Command (set contrast) Double Btye: 1 of 2
    lcd_command(0x20); // Electronic Volume value (contrast value) Double Byte: 2 of 2
    lcd_command(0xA6); // Display normal (non inverser) 
    lcd_command(0xAF); // Display ON 
}

Nous avions également oublié de désactiver le JTAG afin de pouvoir utiliser correctement le PORTF.

MCUCR |= (1 << JTD);
MCUCR |= (1 << JTD);


Nous avons également du allonger notre temps de delay pour notre fonction lcd_reset :

void lcd_reset() {
    PORTF &= ~(1 << RESET);
    _delay_ms(200);
    PORTF |= (1 << RESET);
    _delay_ms(200);
}

Grâce à ces modifications, nous avons réussi à allumer un carré de pixels sur l’écran. Nous nous sommes renseigné pour afficher des objet plus complexe tel qu'un logo. Nous avons récuperer le logo et le code pour afficher une image sur le site du constructeur.

Visualisation code écran avec logo


Affichage d'une image

Pour créer notre propre logo sous forme de tableau il faut s'aider de cette outil : https://javl.github.io/image2cpp/

Voici la configuration à adopter pour exporter correctement sur notre écran :

Image Settings

  • Canvas size(s) = 128x32 (résolution de notre écran)
  • Background color : Black (afin de n’afficher aucune couleur car écran monochrome
  • Scaling : Scale to fit (pour redimensionner l’image selon nos nouvelles proportions)

Les autres paramètres de la section Image Settings restent inchangés (à l’exception de Center image, qui reste une option de personnalisation).

Output

  • Code output format : plain bytes (pour obtenir uniquement les octets qui nous intéressent, on précisera nous-mêmes le type du tableau)
  • Draw mode : Vertical - 1 bit per pixel (l’écran utilise un système de pages pour écrire les pixels)
Visualisation code écran avec logo personnalisé

Et voici la fonction permettant d'afficher une image :

void DispPic(unsigned char* lcd_string)
{
    unsigned char page = 0xB0;
    lcd_command(0xAE); // Display OFF
    lcd_command(0x40); // Display start address + 0x40 (base RAM écran)
    for (unsigned int i = 0; i < 4; i++) { // Parcourt les 4 pages
        lcd_command(page); // Envoie l'adresse de la page actuelle (0xB0 + i)
        lcd_command(0x10); // column address upper 4 bits + 0x10
        lcd_command(0x00); // column address lower 4 bits + 0x00
        for (unsigned int j = 0; j < 128; j++){ // Parcourt toutes les colonnes (128 colonnes)
            lcd_data(*lcd_string); // Envoie un octet de données (une colonne verticale de 8 pixels)
            lcd_string++; // Passe à l'octet suivant dans lcd_string
        }
        page++; // after 128 columns, go to next page
    }
    lcd_command(0xAF);
}

Affichage d'un texte

Pour afficher du texte, nous avons créé un tableau appelé font bitmap en terme courant. Chaque caractère est représenté par une matrice de pixels 5x7, où chaque bit indique si un pixel doit être allumé ou non. Ce format compact nous permet d’afficher les lettres de manière claire et efficace, tout en s’adaptant à la taille souhaitée. Ici nous n'avons qu'une seule taille (5x7) pour répondre à l'objectif embarqué que nous nous sommes fixés (moindre code et moindre consommation).

Voici un tableau donnant une idée de ce à quoi cela peut ressembler :

const uint8_t font5x7[][5] = {
    // ASCII 32 à 127
    {0x00,0x00,0x00,0x00,0x00}, // (space)
    {0x00,0x00,0x5F,0x00,0x00}, // !
    [...]
    {0x00,0x60,0x60,0x00,0x00}, // .
    {0x20,0x10,0x08,0x04,0x02}, // /
    {0x3E,0x51,0x49,0x45,0x3E}, // 0
    {0x00,0x42,0x7F,0x40,0x00}, // 1
    {0x42,0x61,0x51,0x49,0x46}, // 2
    [...]
    {0x7E,0x11,0x11,0x11,0x7E}, // A
    [...]
}

Le tableau font5x7 est organisé en 5 colonnes par caractère parce que chaque caractère est représenté sur une matrice de pixels 5 colonnes (largeur) par 7 lignes (hauteur). Chaque caractère de la police bitmap fait 5 pixels de large et 7 pixels de haut. Ce genre de tableau peut être généré ou repris d'une librairie graphique tel que glcd. Une fois que nous avons le tableau il suffit d'envoyer les données de celle ci ainsi :

void lcd_char(char c) {
    if (c == 248 || c == 176) { // '°' = ASCII étendu 248 ou parfois 176
        for (uint8_t i = 0; i < 5; i++) {
            lcd_data(deg_symbol[i]);
        }
        lcd_data(0x00); // espace
    }
    else if (c >= 32 && c <= 126) {
        for (uint8_t i = 0; i < 5; i++) {
            lcd_data(font5x7[c - 32][i]);
        }
        lcd_data(0x00); // espace
    }
}

void lcd_goto(uint8_t page, uint8_t column) {
    lcd_command(0xB0 | page);            // Page = 0 à 3
    lcd_command(0x10 | (column >> 4));   // MSB
    lcd_command(0x00 | (column & 0x0F)); // LSB
}

void lcd_print(char *str, uint8_t page, uint8_t column) {
    lcd_goto(page, column);
    while (*str) lcd_char(*str++);
}

L'envoie d'un espace après chaque envoie de caractère permet de creer un espacement entre les caractères affichés, pour que les lettres ne soient pas collées les unes aux autres. L’écran est divisé en pages (souvent 8 pixels de hauteur par page, ici on a 4 pages si la hauteur est 32 pixel. Par exemple, page 0 correspond à la ligne verticale 0–7, page 1 à 8–15, etc...

La position horizontale se fait par colonnes (chaque colonne correspondant à une tranche verticale de pixels, souvent 1 octet = 8 pixels en hauteur).

Le contrôleur de l’écran LCD gère en interne une adresse mémoire d’écriture composée d’une page (ligne) et d’une colonne (position horizontale. Le contrôleur incrémente automatiquement la colonne pour la prochaine donnée.

  • lcd_goto() sert à positionner le curseur initial.
  • lcd_data() incrémente la colonne automatiquement après chaque octet envoyé.
Photographie de l'écran avec du texte


Ce projet nous a demandé beaucoup de temps et de persévérance, mais il nous a permis de comprendre en profondeur le fonctionnement d’un écran graphique. Nous sommes désormais capables de coder notre propre bibliothèque pour piloter l’écran, ce qui représentait auparavant un défi majeur.

Communication

Ici, nous traiterons du code implémenté afin de communiquer entre les différents capteurs/actionneurs et notre carte principale. Nous avons utilisé un NRF24L01 pour communiquer entre notre station domotique et nos capteurs/actionneurs. Voici le lien du tutoriel pour l’utilisation de puces à distance : NRF24L01

Ce tutoriel nous a aidés à tester ce module via Arduino. Nous avons d'abord essayé de coder un simple "hello world" à envoyer et recevoir afin de comprendre le fonctionnement du NRF. Ce code permet également de tester les cartes aen cas de dysfonctionnement, comme c'était le cas avec un Arduino fourni par le professeur. Nous avons ensuite utilisé un Arduino qu’un d’entre nous possédait afin de coder.

Par la suite, nous avons codé en C une bibliothèque pour le NRF permettant de communiquer avec celui-ci.

Le fichier .c n'était pas fourni par cette bibliothèque et le fichier .h a été complété par les fonctions ajoutées ci-dessous :

#include <avr/io.h>
#include "nRF24L01.h"

// Définition des ports et broches
#define MISO_DDR DDRB
#define MISO_PORT PORTB
#define MISO_PIN PINB
#define MISO_BIT PB4

#define MOSI_DDR DDRB
#define MOSI_PORT PORTB
#define MOSI_BIT PB3

#define SCK_DDR DDRB
#define SCK_PORT PORTB
#define SCK_BIT PB5

#define CSN_DDR DDRB
#define CSN_PORT PORTB
#define CSN_BIT PB2

#define CE_DDR DDRB
#define CE_PORT PORTB
#define CE_BIT PB1

// Initialisation des broches NRF24L01
void nrf24_setupPins() {
    // MISO en entrée
    MISO_DDR &= ~(1 << MISO_BIT);

    // MOSI, SCK, CSN, CE en sortie
    MOSI_DDR |= (1 << MOSI_BIT);
    SCK_DDR |= (1 << SCK_BIT);
    CSN_DDR |= (1 << CSN_BIT);
    CE_DDR |= (1 << CE_BIT);

    // Valeurs initiales : tout à 0 sauf CSN à 1 (désactivé)
    MOSI_PORT &= ~(1 << MOSI_BIT);
    SCK_PORT &= ~(1 << SCK_BIT);
    CE_PORT &= ~(1 << CE_BIT);
    CSN_PORT |= (1 << CSN_BIT);  // CSN HIGH (non sélectionné)
}

// Contrôle de la broche CE
void nrf24_ce_digitalWrite(uint8_t state) {
    if (state)
        CE_PORT |= (1 << CE_BIT);
    else
        CE_PORT &= ~(1 << CE_BIT);
}

// Contrôle de la broche CSN
void nrf24_csn_digitalWrite(uint8_t state) {
    if (state)
        CSN_PORT |= (1 << CSN_BIT);
    else
        CSN_PORT &= ~(1 << CSN_BIT);
}

// Contrôle de la broche SCK
void nrf24_sck_digitalWrite(uint8_t state) {
    if (state)
        SCK_PORT |= (1 << SCK_BIT);
    else
        SCK_PORT &= ~(1 << SCK_BIT);
}

// Contrôle de la broche MOSI
void nrf24_mosi_digitalWrite(uint8_t state) {
    if (state)
        MOSI_PORT |= (1 << MOSI_BIT);
    else
        MOSI_PORT &= ~(1 << MOSI_BIT);
}

// Lecture de la broche MISO
uint8_t nrf24_miso_digitalRead() {
    return (MISO_PIN & (1 << MISO_BIT)) ? 1 : 0;
}

Nous avons déclaré en #define les DDR, PORT, PIN et BIT de chaque broche afin d'avoir un code plus lisible.

Initialisation des broches du NRF

Pour commencer, nous avons nrf24_init() qui sert à configurer les broches utilisées par le module (MISO, MOSI, SCK, CSN, CE). Ça permet de préparer la communication SPI logicielle. Ensuite, on mets CE à LOW et CSN à HIGH, ce qui correspond à l’état « repos » du module.


Configuration du module NRFLa fonction nrf24_config sert à configurer le module selon le canal radio (fréquence) et la taille des paquets (payload).

  • Elle la longueur de la charge utile (payload) dans une variable globale.
  • Elle configure les différents registres : le canal RF, la taille du payload pour les pipes (canaux de réception), la puissance d’émission, le CRC, l’auto-acknowledgment (reconnaissance automatique de réception), les adresses RX activées, la retransmission automatique.
  • Puis elle mets le module en mode écoute (réception).


Gestion des adresses TX et RX

nrf24_tx_address() et nrf24_rx_address() servent à définir les adresses pour l’envoi et la réception. Ces adresses doivent être cohérentes pour que la communication fonctionne. Envoi et réception des données

Envoi et réception des données

nrf24_send() permet d’envoyer un paquet. Elle prépare le module, vide le FIFO d’émission, puis écris le payload et démarre la transmission.

nrf24_getData() lit les données reçues depuis le module en SPI, puis remet à zéro le flag d’interruption réception.


Vérification de l’état

nrf24_dataReady() et nrf24_rxFifoEmpty() permettent de savoir si des données sont prêtes à être lues.

nrf24_isSending() indique si le module est encore en train d’envoyer un message.

nrf24_lastMessageStatus() dit si la dernière transmission a réussi ou a échoué (nombre max de retransmissions atteint).

nrf24_retransmissionCount() donne le nombre de tentatives de retransmission.


Gestion de la puissance

nrf24_powerUpRx(), nrf24_powerUpTx(), nrf24_powerDown() sont des fonctions pour mettre le module en mode réception, émission, ou veille.


Communication SPI en logiciel (bit-banging)

Comme on n’utilise pas le matériel SPI natif, spi_transfer() envoie et reçoit un octet via manipulation manuelle des broches MOSI, MISO et SCK.

Les fonctions nrf24_transferSync() et nrf24_transmitSync() permettent d’envoyer ou recevoir plusieurs octets à la suite. Cela à été fait de cette façon afin d'avoir le code le plus portatif possible, ce qui explique le contenu de cette librairie. Nous n'avons pas implémenté le SPI matériel puisque le code fonctionne très bien sans. Lecture/écriture des registres Pour lire ou écrire un registre du nRF24, nrf24_readRegister() et nrf24_writeRegister(), envoient la commande adéquate en SPI puis récupèrent ou envoient les données.


Nous avons commencé par étudier la documentation officielle du module nRF24L01, en particulier le datasheet, afin de comprendre le protocole SPI, les registres internes et les commandes à utiliser. Ensuite, nous avons consulté différents exemples sur GitHub ainsi que des tutoriels pour Arduino.


Je ne vais pas remontrer la vidéo de démonstration c'est redondant ici. Il y en aura une pour la prochaine étape qui est ...

IHM PC

Dans cette section, nous expliquons comment la carte domotique communique avec un PC via USB, en utilisant la bibliothèque LUFA. Le code utilisé est un exemple issu du repertoire suivant, déjà employé dans un projet annexe de programmateur AVR. Ce code permet l’envoi simple de données via une liaison série USB (USB CDC).

Pour récupérer ces données côté PC et les afficher, nous avons choisi d’utiliser d’abord Node-RED pour la gestion des flux de données, puis Grafana (outil recommandé par M. Boé) pour l’affichage graphique qui sera implémenté plus tard. Ce choix nous permet de gagner du temps sur la partie interface web, qui peut être longue à développer manuellement.

Grafana ne sera peut être pas déployé car la liaison entre Node-RED et Grafana doit se faire depuis une base de donnée (surement InfluxDB) et cela prend du temps à être mis en place.

Voici le repertoire de cette partie : https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/03%20-%20%20ui_web_interface

LUFA

Ce code permet de faire un Hello World. Il a été nettoyé au préalable. Il est également disponible dans ce repertoire.

#include "VirtualSerial.h"

static CDC_LineEncoding_t LineEncoding = { .BaudRateBPS = 0,
                                           .CharFormat  = CDC_LINEENCODING_OneStopBit,
                                           .ParityType  = CDC_PARITY_None,
                                           .DataBits    = 8 };

int main(void)
{
    SetupHardware();
    LEDs_SetAllLEDs(LEDMASK_USB_NOTREADY);
    GlobalInterruptEnable();

    for (;;)
    {
        CDC_Task();
        USB_USBTask();
    }
}

void SetupHardware(void)
{
    MCUSR &= ~(1 << WDRF);
    wdt_disable();
    clock_prescale_set(clock_div_1);

    USB_Init();
}

void EVENT_USB_Device_Connect(void)
{
    LEDs_SetAllLEDs(LEDMASK_USB_ENUMERATING);
}

void EVENT_USB_Device_Disconnect(void)
{
    LEDs_SetAllLEDs(LEDMASK_USB_NOTREADY);
}

void EVENT_USB_Device_ConfigurationChanged(void)
{
    bool ConfigSuccess = true;
    ConfigSuccess &= Endpoint_ConfigureEndpoint(CDC_NOTIFICATION_EPADDR, EP_TYPE_INTERRUPT, CDC_NOTIFICATION_EPSIZE, 1);
    ConfigSuccess &= Endpoint_ConfigureEndpoint(CDC_TX_EPADDR, EP_TYPE_BULK, CDC_TXRX_EPSIZE, 1);
    ConfigSuccess &= Endpoint_ConfigureEndpoint(CDC_RX_EPADDR, EP_TYPE_BULK, CDC_TXRX_EPSIZE, 1);
    LineEncoding.BaudRateBPS = 0;
    LEDs_SetAllLEDs(ConfigSuccess ? LEDMASK_USB_READY : LEDMASK_USB_ERROR);
}

void EVENT_USB_Device_ControlRequest(void)
{
    switch (USB_ControlRequest.bRequest)
    {
        case CDC_REQ_GetLineEncoding:
            if (USB_ControlRequest.bmRequestType == (REQDIR_DEVICETOHOST | REQTYPE_CLASS | REQREC_INTERFACE))
            {
                Endpoint_ClearSETUP();
                Endpoint_Write_Control_Stream_LE(&LineEncoding, sizeof(LineEncoding));
                Endpoint_ClearOUT();
            }
            break;
        case CDC_REQ_SetLineEncoding:
            if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
            {
                Endpoint_ClearSETUP();
                Endpoint_Read_Control_Stream_LE(&LineEncoding, sizeof(LineEncoding));
                Endpoint_ClearIN();
            }
            break;
        case CDC_REQ_SetControlLineState:
            if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
            {
                Endpoint_ClearSETUP();
                Endpoint_ClearStatusStage();
            }
            break;
    }
}

void CDC_Task(void)
{
    // Vérifie si le périphérique USB est bien configuré avant de continuer
    if (USB_DeviceState != DEVICE_STATE_Configured)
        return;

    // Chaîne de caractères à envoyer sur la liaison série USB
    char msg[] = "Hello world\r\n";

    // Sélectionne l'endpoint d'envoi (TX) pour préparer l'envoi de données
    Endpoint_SelectEndpoint(CDC_TX_EPADDR);

    // Écrit le message dans le buffer de l'endpoint USB, en mode Little Endian
    Endpoint_Write_Stream_LE(msg, sizeof(msg)-1, NULL);

    // Vérifie si le buffer de l'endpoint est plein après l'écriture
    bool full = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);

    // Vide (envoie) le contenu du buffer IN vers l'hôte
    Endpoint_ClearIN();

    // Si le buffer était plein, attend que l'endpoint soit prêt pour un autre envoi
    if (full)
    {
        // Attend que l'endpoint soit prêt pour un nouvel envoi (acknowledgement de l'hôte)
        Endpoint_WaitUntilReady();

        // Vide de nouveau le buffer pour s'assurer que tout est bien envoyé
        Endpoint_ClearIN();
    }

    // Sélectionne l'endpoint de réception (RX) pour traiter d'éventuelles données entrantes
    Endpoint_SelectEndpoint(CDC_RX_EPADDR);

    // Si des données ont été reçues par l'hôte on vide le buffer
    if (Endpoint_IsOUTReceived())
        Endpoint_ClearOUT();

    // Pause de 300 ms pour ralentir l'exécution de la tâche (évite les envois en boucle trop rapides)
    _delay_ms(300);
}

Nous n'avons pas réussi à souder notre NRF, malheureusement le code avec celui ne fonctionnera donc pas. Le code avec NRF sera fait avec un Arduino Uno et UART comme l'atmega328p ne supporte pas la LUFA.

Voici tout de même une piste qui pourrait fonctionner : https://gitea.plil.fr/ahouduss/se3_2024_B2/src/branch/main/02%20-%20Station%20Domotique/02%20-%20Programmation/CapteurHumidite_ET_NRF/Lufa/SE/VirtualSerial

Docker

Ces outils sont déployés à l’aide de Docker, une technologie (open source et créée par des ingénieurs français !) de virtualisation légère qui permet d’exécuter des applications dans des conteneurs isolés.

Docker permet de packager toute une application et ses dépendances dans un conteneur. Plus besoin de réinstaller des bibliothèques, configurer l’environnement, ou se soucier du “ça marche sur mon PC mais pas ailleurs”. Cela marchera donc aussi de votre côté ;).

Nous utilisons également Docker Compose pour automatiser le lancement coordonné de plusieurs services (ici Node-RED et Grafana) à partir d’un simple fichier de configuration.

Voici le fichier de configuration de Docker Compose :

version: '3.8'

services:

  nodered:
    image: nodered/node-red:latest
    container_name: nodered
    ports:
      - "1880:1880"
    volumes:
      - ./nodered_data:/data
    devices:
      - /dev/ttyACM0
    restart: unless-stopped

  grafana:
    image: grafana/grafana-oss
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - ./grafana_data:/var/lib/grafana


    restart: unless-stopped

On configure le port où le service sera lancé et nous laissons les accès devices à nodered pour quelle puisse écouter le port série où notre LUFA écrit. La mention - ${SERIAL_DEV:-/dev/null}:/dev/ttyACM0 peut être ajouté afin de pouvoir lancer le dock sans problème de compilation car Docker ne démarre pas le conteneur si un périphérique mentionné dans devices: est introuvable.

La mention${SERIAL_DEV:-/dev/null}: créera un lien vers /dev/null (un périphérique vide), évitant ainsi l’erreur. Je connaissais Docker parceque j'utilisais une application qui se lançait sur celle-ci et je m'y suis intéressé. J'estimais intéressant de l'intégrer au projet !

Voici un fichier mémo qui nous a aider à nous rappeler des commandes importantes sur Docker :

Liste des docker actif sur le pc :
docker ps -a

Dans ce repertoire, lancer les dockers via :
docker compose up -d

Pour relancer copie tout :
docker compose down
docker compose up -d

Pour stopper un docker : 
docker stop <nomDock>

Pour supprimer un docker :
docker rm -f <nomDock>

Si un pb, voir log :
docker logs <nomDock>

Node-RED

Voici à quoi ressemble notre configuration :

Configuration Node-RED


Bloc 1 : Entrée série (Serial In)

Ce bloc permet d'écouter un port série. Il lit les données envoyées par la carte domotique sur le port /dev/ttyACM0. Les données sont transmises sous forme de texte brut, souvent une chaîne JSON. Voici la configuration :

Configuration bloc 1


Bloc 2 : Conversion JSON

Les données reçues sont des chaînes de caractères au format JSON. Ce bloc les convertit en objet JavaScript pour que Node-RED puisse les manipuler plus facilement dans les blocs suivants.


Bloc 3 : Traitement de la donnée (Function)

Ce bloc exécute une petite fonction JavaScript pour isoler la température contenue dans l'objet JSON.

Voici le contenu de la fonction :

msg.payload = msg.payload.temperature;
return msg;
  • msg.payload contient l'objet JSON complet, par exemple : { "temperature": 22.5, "...": 45, "...": 20, "...": "oui" }
  • La ligne msg.payload = msg.payload.temperature; remplace le contenu du message pour ne garder que la valeur de la température (ici 22.5). Cela permet d’envoyer uniquement la température au bloc suivant, comme une simple valeur numérique.
  • return msg; renvoie ce nouveau message modifié pour qu’il continue à circuler dans le flow.


Bloc 4 : Affichage ou base de données

Le message contenant uniquement la température peut ensuite être affiché dans un tableau de bord, envoyé à Grafana, ou enregistré dans une base de données.

Pour le moment on utilisera pas de base donnée mais une interface beaucoup plus minimaliste sur Node-RED.

Voici la configuration du bloc :

Configuration bloc 3 permettant l'affichage de la température

Pour avoir accès au noeud du bloc 1 et 3 il a fallu ajouter des "nodes" à notre palette. Pour y acceder c'est ici :

Acces à Palette

Et voici les noeuds installés :

Palette de nodes du projet


Maintenant regardons notre site final :

Accès au résultat de notre Node-RED
Accès vue sur notre projet





On clique sur la petite icône est nous sommes renvoyé sur cette url : http://localhost:1880/ui/

Voici notre interface finale :

Dashboard site web

Et une démonstration du projet dans son intégralité en vidéo ci dessous :

Vidéo demonstration du projet fonctionnel

Programmateur AVR (Projet annexe)

Objectif

Réaliser un programmateur AVR afin d'envoyer notre code C sur un microcontrôleur. Ce projet est une introduction au logiciel et à la programmation sur microcontroleur AVR.

Nous nous sommes aider du cours afin de réaliser notre projet : https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme063.html

Schématique

Notre schéma électrique

Schéma électrique KICAD

Conception de notre schéma électrique

Document expliquant point par point le schéma réalisé sur KICAD

Documents relatifs à la conception du kicad de la carte

Datasheet ATMEGA8U2
AVR Hardware Design Considerations

Vue 3D

Programmmateur AVR - 3D KICAD

Fichier kicad

Fichier:2024-PSE-G2-Prog VFinale sans erreur.zip

Brasure

Carte programmateur AVR - Vue avant
Carte programmateur AVR - Vue arrière

Programmation

Test leds et boutons

Afin de vérifier que notre carte fonctionne correctement après sa brasure, on code un programme permettant d'allumer une LED lorsqu'un bouton poussoir est pressé. Chaque bouton est associé à une LED.

Modification de l'horloge
void setupClock(void)
{
	CLKSEL0 = 0b00010101;   // sélection de l'horloge externe
	CLKSEL1 = 0b00001111;   // minimum de 8Mhz
	CLKPR = 0b10000000;     // modification du diviseur d'horloge (CLKPCE=1)
	CLKPR = 0;              // 0 pour pas de diviseur (diviseur de 1)
}
Fonction initialisation des pins
void setupPin(volatile uint8_t* PORTx, volatile uint8_t* DDRx, uint8_t pin, pinmode mode) {
    switch (mode)
    {
    case INPUT: // Forcage pin à 0
        *DDRx &= ~(1 << pin);
        break;
    case INPUT_PULL_UP: // Forcage pin à 0
        *DDRx &= ~(1 << pin);
        *PORTx |= (1 << pin);
        break;
    case OUTPUT: // Forcage pin à 1
        *DDRx |= (1 << pin);
        break;
    }
}
Fonction initialisation de l'ensemble des boutons et LEDs
void setupHardware() {
    setupClock();

    setupPin(LEDs_PORT, LEDs_DDR, LED1_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED2_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED3_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED4_PIN, OUTPUT);

    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Up_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Down_PIN, INPUT_PULL_UP);
    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Left_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Right_PIN, INPUT_PULL_UP);
}
Fonction de lecture de pin
int readPin(volatile uint8_t* PINx, uint8_t pin) {
    return (*PINx & (1 << pin));
}
Fonction d'écriture de pin
void writePin(volatile uint8_t* PORTx, uint8_t pin, write level) {
    if (level == LOW)
        *PORTx |= (1 << pin);
    else
        *PORTx &= ~(1 << pin);
}
Fonction pour initialiser les composantes de la cartes
// ------------------ Boutons ------------------ //
#define BTNsUp_Left_PORT &PORTC
#define BTNsUp_Left_DDR &DDRC
#define BTNsUp_Left_PIN &PINC

#define BTNsDown_Right_PORT &PORTB
#define BTNsDown_Right_DDR &DDRB
#define BTNsDown_Right_PIN &PINB

#define BTN_Up_PIN PC4 
#define BTN_Down_PIN PB5 
#define BTN_Left_PIN PC6 
#define BTN_Right_PIN PB6 

// ------------------ LEDs ------------------ //
#define LEDs_PORT &PORTD
#define LEDs_DDR &DDRD
#define LEDs_PIN &PIND

#define LED1_PIN PD0
#define LED2_PIN PD1
#define LED3_PIN PD2
#define LED4_PIN PD3

// ------------------ Enum ------------------ //
typedef enum {
    INPUT,
    INPUT_PULL_UP,
    OUTPUT,
} pinmode;


void setupHardware() {
    setupClock();

    setupPin(LEDs_PORT, LEDs_DDR, LED1_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED2_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED3_PIN, OUTPUT);
    setupPin(LEDs_PORT, LEDs_DDR, LED4_PIN, OUTPUT);

    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Up_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Down_PIN, INPUT_PULL_UP);
    setupPin(BTNsUp_Left_PORT, BTNsUp_Left_DDR, BTN_Left_PIN, INPUT_PULL_UP);
    setupPin(BTNsDown_Right_PORT, BTNsDown_Right_DDR, BTN_Right_PIN, INPUT_PULL_UP);
}

LUFA

Afin de pouvoir faire de notre carte un périphérique USB, nous allons utiliser la LUFA (Lightweight USB Framefork for AVRs).

Pour écrire notre code, nous avons réutilisé l'exemple VirtualSerial récupéré au repertoire suivant : LUFA/Demos/Device/LowLevel/VirtualSerial


Le code exemple du VirtualSerial permet d'écrire des données à notre pc (visible via minicom).


Afin de mettre le programme sur notre carte, on vérifie au préalable que notre carte est bien reconnue en tant que périphérique USB à l'aide de la commande lsusb.

Terminal - cmd lsusb avec carte.png
Terminal - Cmd lsusb.png

Pour plus de détail sur notre périphérique usb branché on doit faire la commande suivante :

cedricagathe@computer:~$ lsusb -s 003:008 -v

Bus 003 Device 011: ID 03eb:2044 Atmel Corp. LUFA CDC Demo Application
[...]
Device Descriptor:
  bLength                18
  bDescriptorType         1
[...]
  bDeviceClass            2 Communications
  bDeviceSubClass         0 
  bDeviceProtocol         0 
[...]
  idVendor           0x03eb Atmel Corp.
  idProduct          0x2044 LUFA CDC Demo Application
[...]
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         2 Communications
      bInterfaceSubClass      2 Abstract (modem)
      bInterfaceProtocol      1 AT-commands (v.25ter)
[...]
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x82  EP 2 IN
[...]
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        1
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass        10 CDC Data
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
[...]
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x83  EP 3 IN
 [...]

Comme vu en cours, on revoit nos descripteurs ainsi que la description des interfaces de l'usb ainsi que d'autres données. Une fois notre carte détectée, on appuie en continue sur le bouton HWB puis une seule impulsion sur le bouton RESET afin de mettre notre carte en mode DFU et ensuite on relache HWB.

On pourra alors téléverser notre programme sur la carte à l'aide d'un makefile préalablement codé en tapant la commande make dfu.

  • On place le programme dans notre carte à l'aide de la commande make upload. Pour ce faire, il faut modifier le makefile original par ceci :
MCU          = atmega8u2
ARCH         = AVR8
BOARD        = NONE
F_CPU        = 16000000
F_USB        = $(F_CPU)
OPTIMIZATION = s
TARGET       = VirtualSerial
SRC          = $(TARGET).c Descriptors.c spi.c $(LUFA_SRC_USB)
LUFA_PATH    = ../../LUFA
CC_FLAGS     = -DUSE_LUFA_CONFIG_HEADER -IConfig/
LD_FLAGS     =
PROGRAMMER  = dfu-programmer

# Include LUFA-specific DMBS extension modules
DMBS_LUFA_PATH ?= $(LUFA_PATH)/Build/LUFA
include $(DMBS_LUFA_PATH)/lufa-sources.mk
include $(DMBS_LUFA_PATH)/lufa-gcc.mk

# Include common DMBS build system modules
DMBS_PATH      ?= $(LUFA_PATH)/Build/DMBS/DMBS
include $(DMBS_PATH)/core.mk
include $(DMBS_PATH)/cppcheck.mk
include $(DMBS_PATH)/doxygen.mk
include $(DMBS_PATH)/dfu.mk
include $(DMBS_PATH)/gcc.mk
include $(DMBS_PATH)/hid.mk
include $(DMBS_PATH)/avrdude.mk
include $(DMBS_PATH)/atprogram.mk

Le MCU a été changé afin de correspondre au nôtre. Une commande clean à été ajouté et la fréquence CPU augmentée à 16 MHz.

  • On lance la commande minicom afin de voir si le bouton pressé est bien affiché via la liaison série. On configure le minicom au préalable.
sudo minicom -os

Ensuite nous entrons dans Serial port setup :

Image terminal apres commande minicom -os.png
Menu serial port ssetup minicom.png

Il va falloir procéder à 3 changements :

  1. Appuyer sur A et changer modem par l'emplacement de notre carte (ici /ttyACM0) puis ENTRER
  2. Appuyer sur E puis appuyer sur C pour configurer la vitesse de notre minicom puis ENTRER
  3. Appuyer sur F puis ENTRER

Appuyer de nouveau sur ENTRER pour enregistrer la configuration et ensuite faite Exit. On est maintenant prêt à recevoir les messages USB qu'on envoie lors d'un appui :

Interface minicom vierge.png
Interface minicom apres reception message.png

  • Nous avons ensuite implémenté un programme permettant d'allumer une led différente à chaque bouton-poussoir pressé.

Demonstration vidéo de notre programme utilisant la LUFA

Modification du fichier virtual.c
Afficher sur le minicom lorsqu'un bouton est pressé et test leds
void CDC_Task(void) {
	static char ReportBuffer[64]; // Buffer pour stocker le message à envoyer
	static bool ActionSent = false; // Pour éviter d'envoyer plusieurs fois le même message

	bool hasMessage = false;  // Indique si un bouton a été pressé

	// Vérifie que l’appareil est connecté et configuré
	if (USB_DeviceState != DEVICE_STATE_Configured)
		return;

	// Détection du bouton haut
	if (!readPin_HardwareProgAVR(BTNsUp_Left_PIN, BTN_Up_PIN)) {
		hasMessage = true;
		strcpy(ReportBuffer, "bouton du haut\r\n");

		toggleLed(LEDs_PORT, LEDs_PIN, LED1_PIN);
		_delay_ms(300); // Anti-rebond
	}
	else ActionSent = false;


	// Détection du bouton bas
	if (!readPin_HardwareProgAVR(BTNsDown_Right_PIN, BTN_Down_PIN)) {
		hasMessage = true;
		strcpy(ReportBuffer, "bouton du bas\r\n");

		toggleLed(LEDs_PORT, LEDs_PIN, LED2_PIN);

		_delay_ms(300);
	}
	else ActionSent = false;

	// Détection du bouton gauche
	if (!readPin_HardwareProgAVR(BTNsUp_Left_PIN, BTN_Left_PIN)) {
		hasMessage = true;
		strcpy(ReportBuffer, "bouton de gauche\r\n");

		toggleLed(LEDs_PORT, LEDs_PIN, LED3_PIN);
		_delay_ms(300);

	}
	else ActionSent = false;

	// Détection du bouton droit
	if (!readPin_HardwareProgAVR(BTNsDown_Right_PIN, BTN_Right_PIN)) {
		hasMessage = true;
		ReportString = "bouton de droite\r\n";
		// spi_test_octet(ReportBuffer);
		// getIDspi(ReportBuffer);

		toggleLed(LEDs_PORT, LEDs_PIN, LED4_PIN);
		_delay_ms(300);
	}
	else ActionSent = false;

	// Si un bouton a été pressé et qu'aucune action n’a encore été envoyée
	if (hasMessage && (ActionSent == false) && LineEncoding.BaudRateBPS) {
		ActionSent = true;

		// Envoie le message via USB
		Endpoint_SelectEndpoint(CDC_TX_EPADDR);
		Endpoint_Write_Stream_LE(ReportBuffer, strlen(ReportBuffer), NULL);

		bool IsFull = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);
		Endpoint_ClearIN();

		if (IsFull) {
			Endpoint_WaitUntilReady();

			Endpoint_ClearIN();
		}
	}

	// Nettoie le buffer de réception (inutile ici mais bonne pratique)
	Endpoint_SelectEndpoint(CDC_RX_EPADDR);
	if (Endpoint_IsOUTReceived())
		Endpoint_ClearOUT();
}

L’ajout d’un délai de 300 ms est nécessaire pour éviter les rebonds des boutons mécaniques. Sans cela, une pression peut être détectée plusieurs fois à cause des oscillations électriques rapides à l’activation.

Ici on voit que le code peut être factorisé. Cela n'a pas été fais pour tester individuellement des fonctions différentes sur chaque bouton (cf prochaine section).

Récupération ID microcontroleur

Il y a des tentatives afin de récuprer un identifiant d'un microcontroleur via la fonction getIDspi(ReportBuffer).

#include "../spi.h"

#include <avr/io.h>
#include <string.h>
#include <util/delay.h>


#define SPI_DDR         DDRB
#define SPI_PORT        PORTB
#define SPI_SS          PB0
#define SPI_SCK         PB1
#define SPI_MOSI        PB2
#define SPI_MISO        PB3

void spi_init(void) {                                 // Initialisation du bus SPI
    SPI_DDR |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS);   // Définition des sorties
    SPI_DDR &= ~(1 << SPI_MISO);                           // Définition de l'entrée
    SPI_PORT |= (1 << SPI_SS);                             // Désactivation du périphérique
    SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0);       // Activation SPI (SPE) en état maître (MSTR)
    SPSR &= ~(1 << SPI2X);                                // horloge F_CPU/128 (SPI2X=0, SPR1=1,SPR0=1)
}

void spi_activer(void) {                              // Activer le périphérique
    SPI_PORT &= ~(1 << SPI_SS);                            // Ligne SS à l'état bas
}

void spi_desactiver(void) {                           // Désactiver le périphérique
    SPI_PORT |= (1 << SPI_SS);                             // Ligne SS à l'état haut
}

uint8_t spi_echange(uint8_t envoi) {    // Communication sur le bus SPI
    SPDR = envoi;                                                   // Octet a envoyer
    while (!(SPSR & (1 << SPIF)));                                     // Attente fin envoi (drapeau SPIF du statut)
    return SPDR;                                                    // Octet reçu
}

uint8_t spi_transaction(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
    spi_echange(a);
    spi_echange(b);
    spi_echange(c);
    return spi_echange(d);
}

void end_pmode(void) {
    PORTB &= ~(1 << PB0);
}

#define nibble2char(n)  (((n)<10)?'0'+(n):'a'+(n)-10)

void convert(unsigned char byte, char* string) {
    string[0] = nibble2char(byte >> 4);
    string[1] = nibble2char(byte & 0x0f);
    string[2] = '\0';
}

void getIDspi(char* ReportString) {
    spi_activer();
    _delay_ms(10); // délai optionnel pour stabilité

    uint8_t high = spi_transaction(0x30, 0x00, 0x00, 0x00); //cf p8 Device Code de la DS AVR_ISP
    spi_desactiver();
    spi_activer();
    _delay_ms(10); // délai optionnel pour stabilité
    // convert(high, format + 5);  // "xx" remplacé par la valeur convertie

    uint8_t middle = spi_transaction(0x30, 0x00, 0x01, 0x00);
    spi_desactiver();
    spi_activer();
    _delay_ms(10); // délai optionnel pour stabilité
    // convert(middle, format + 14);  // "yy" remplacé par middle
    uint8_t low = spi_transaction(0x30, 0x00, 0x02, 0x00);
    spi_desactiver();
    // convert(low, format + 22);     // "zz" remplacé par low



    char highHex[3], middleHex[3], lowHex[3];
    convert(high, highHex);
    convert(middle, middleHex);
    convert(low, lowHex);

    // Construire la chaîne manuellement
    char* ptr = ReportString;
    const char* prefix = "RAW SPI ID: H=0x";
    while (*prefix) *ptr++ = *prefix++;

    *ptr++ = highHex[0];
    *ptr++ = highHex[1];

    const char* midStr = " M=0x";
    while (*midStr) *ptr++ = *midStr++;

    *ptr++ = middleHex[0];
    *ptr++ = middleHex[1];

    const char* lowStr = " L=0x";
    while (*lowStr) *ptr++ = *lowStr++;

    *ptr++ = lowHex[0];
    *ptr++ = lowHex[1];

    *ptr++ = '\r';
    *ptr++ = '\n';
    *ptr = '\0';
}

Avec l’aide de M. Redon, nous avons tenté de récupérer l’identifiant unique d’un microcontrôleur AVR en utilisant le protocole SPI. Pour cela, nous avons développé un programme basé sur une communication SPI bas-niveau, avec les fonctions d’initialisation, d’activation/désactivation du bus, et d’échange de données via SPI.

Le principe était d’envoyer des commandes spécifiques conformes au protocole ISP (In-System Programming) d’Atmel, notamment l’envoi de la commande 0x30 suivie d’adresses pour lire les octets composant l’ID (haut, milieu, bas). La fonction getIDspi() réalise ces transactions successives et convertit les valeurs reçues en chaîne hexadécimale lisible.

Nous avons également implémenté une fonction de test simple, spi_test_octet(), qui envoie un octet 0x55 et devrait recevoir la même valeur en retour si le périphérique répond correctement.

void spi_test_octet(char* ReportString) {
    // Configuration de RESET en sortie
    SPI_DDR |= (1 << SPI_SS);          // RESET = PB0 en sortie
    SPI_PORT &= ~(1 << SPI_SS);        // RESET à 0 : mode programmation
    // _delay_ms(30);                     // Attente suffisante (20ms minimum)

    // Envoie de la commande ISP pour vérifier si la cible répond
    // spi_activer();                     // SS SPI actif (LOW)
    // uint8_t check = spi_transaction(0xAC, 0x53, 0x00, 0x00);  // Activation ISP
    // spi_desactiver();                 // SS SPI inactif (HIGH)

    // if (check != 0x53) {
    //     strcpy(ReportString, "Erreur: mode ISP non actif\r\n");
    //     SPI_PORT |= (1 << SPI_SS);      // RESET à 1 : fin prog
    //     return;
    // }

    // Si la cible est bien en mode programmation, test SPI
    spi_activer();
    uint8_t response = spi_echange(0x55);
    spi_desactiver();

    SPI_PORT |= (1 << SPI_SS);  // Fin du mode ISP (RESET à 1)

    // Construction de la chaîne
    char hexStr[3];
    convert(response, hexStr);

    char* ptr = ReportString;
    const char* prefix = "SPI test response: 0x";
    while (*prefix) *ptr++ = *prefix++;
    *ptr++ = hexStr[0];
    *ptr++ = hexStr[1];
    *ptr++ = '\r';
    *ptr++ = '\n';
    *ptr = '\0';
}

Cependant, malgré plusieurs essais, la récupération stable de l’ID n’a pas été réussie :

  • Le signal SPI semble fonctionner de manière intermittente.
  • La réponse attendue (0x55) est parfois reçue, mais de façon instable.
  • Cela pourrait venir d’un problème matériel (câblage, niveau des signaux) ou d’un timing non respecté dans le protocole SPI/ISP.
  • Fonctions SPI implémentées :
    • spi_init() : configure les broches et paramètres SPI maître.
    • spi_activer() / spi_desactiver() : contrôle de la ligne SS (Slave Select).
    • spi_echange() : envoie et réception d’un octet SPI.
    • spi_transaction() : envoie une séquence de quatre octets en SPI, utile pour la commande ISP.


Jusqu'ici, je n'ai pas de piste pour pouvoir continuer, sachant que le câblage n'est pas le problème.