« SE3Groupe2024-2 » : différence entre les versions
(→IHM PC) |
|||
(124 versions intermédiaires par 3 utilisateurs non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
== Description == | == 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 === | === Objectif === | ||
L'objectif de ce projet est de concevoir une station domotique capable de collecter et d'afficher des mesures provenant de capteurs | 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 === | === Cahier des charges === | ||
La station domotique devra permettre l'affichage des informations suivantes concernant une pièce : | La station domotique devra permettre l'affichage des informations suivantes concernant une pièce : | ||
* Température ambiante ; | * Température ambiante ; | ||
* Taux d'humidité ; | * Taux d'humidité ; | ||
Ligne 13 : | Ligne 15 : | ||
Elle devra aussi permettre de contrôler différents actionneurs dans la pièce, tels que : | 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) ; | * 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. | * 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. | |||
Des capteurs et actionneurs supplémentaires pourront être ajoutés si le projet atteint ses objectifs initiaux. | |||
=== Spécification techniques === | === Spécification techniques === | ||
Le projet nécessite un microcontrôleur, qui contiendra le programme, et qui communiquera avec les autres composants via les ''GPIOs'' | ==== 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 <u>plusieurs modèles de microcontrôleur</u> : '''ATmega16u4, AT90USB1286, AT90USB1287.''' | |||
Voici un tableau comparatif afin de sélectionner le plus adapté pour notre usage : | Voici un tableau comparatif afin de sélectionner le plus adapté pour notre usage : | ||
Ligne 99 : | Ligne 98 : | ||
|https://www.microchip.com/en-us/product/at90usb1287 | |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. | |||
{| class="wikitable" | |||
!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 :'' | |||
[[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 ==== | |||
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 : [https://passionelectronique.fr/tutorial-nrf24l01 NRF24L01] | |||
''Datasheet NRF24L01 :'' | |||
[[Fichier:Datasheet NRF24L01.pdf|200x200px|vignette|Datasheet module de communication : NRF24L01|centré]] | |||
<p style="clear: both;" /> | |||
==== Énergie ==== | ==== Énergie ==== | ||
La station sera alimentée de manière hybride, selon les scénarios suivants : | 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 : | 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. | |||
<u>Modèles de batterie à notre disposition :</u> | <u>Modèles de batterie à notre disposition :</u> | ||
* Batterie 3.7V 100 mAh, connecteur molex mâle ; | * Batterie 3.7V 100 mAh, connecteur molex mâle ; | ||
* Batterie 3.7V 300 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 :''[[Fichier:Datasheet MAX1811.pdf|gauche|194x194px|vignette|Datasheet du chargeur : MAX1811]] | |||
* | |||
[[Fichier:Datasheet LTC3531.pdf|194x194px|vignette|Datasheet du régulateur : LTC3531|centré]] | |||
<p style="clear: both;" /> | |||
==== Affichage ==== | ==== Affichage ==== | ||
Ligne 129 : | Ligne 190 : | ||
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. | 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é :'' | |||
''' | [[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 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.<p style="clear: both;" /> | |||
== | ==== 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. | |||
<p style="clear: both;" /> | |||
== | == Hardware == | ||
=== Schématique === | === Schématique === | ||
==== Notre schéma électrique ==== | |||
[[Fichier:Kicad station .pdf|centré|vignette|Schéma électrique V1 KICAD]] | |||
<p style="clear: both;" /> | |||
==== Comprendre notre schéma ==== | |||
[[Fichier:ComprendreSchematique.pdf|centré|vignette|Comprendre la schématique]] | |||
<p style="clear: both;" /> | |||
=== Vue 3D === | |||
[[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|432x432px]] | |||
<p style="clear: both;" /> | |||
=== Brasure === | === Brasure === | ||
== Capteurs == | <p style="clear: both;" /> | ||
== 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. | |||
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 ===== | |||
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;" /> | |||
[[Fichier:Datasheet mvmt.pdf|alt=datasheet_mvmt|vignette|datasheet_mvmt]] | |||
[[Fichier:Mvmt.png|alt=mvmt|vignette|capteur_mvmt|479x479px|gauche]] | |||
<p style="clear: both;" /> | |||
* 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. <syntaxhighlight lang="c" line="1"> | |||
#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; | |||
} | |||
</syntaxhighlight> | |||
<p style="clear: both;" /> | |||
===== 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. | |||
[[Fichier:Capteur presence.mp4|centré|vignette|500px|Capteur presence]] | |||
<p style="clear: both;" /> | |||
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;" /> | |||
==== <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 ===== | |||
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é]] | |||
<p style="clear: both;" /> | |||
===== 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).[[Fichier:Capteur eau.jpg|alt=capteur_eau|vignette|capteur_eau|centré]] | |||
<p style="clear: both;" /> | |||
===== Programmation ===== | |||
====== Arduino ====== | |||
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 ====== | |||
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 : [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] | |||
'''''<u>Protocole OneWire</u>''''' | |||
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 [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 ===== | |||
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.[[Fichier:Capteur temperature.mp4|centré|vignette|video capteur eau]] | |||
<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 la température de la pièce sur l'écran. | |||
<p style="clear: both;" /> | |||
=== <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); | |||
// 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); | |||
} | |||
</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 === | |||
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 ==== | |||
[[Fichier:SE3_2024_G2_prog_schema.pdf|center|thumb|Schéma électrique KICAD]] | |||
<p style="clear: both;" /> | |||
==== Conception de notre schéma électrique ==== | |||
[[Fichier:Comprendre le schéma.pdf|vignette|centré|Document expliquant point par point le schéma réalisé sur KICAD]] | [[Fichier:Comprendre le schéma.pdf|vignette|centré|Document expliquant point par point le schéma réalisé sur KICAD]] | ||
<p style="clear: both;" /> | |||
'''Documents relatifs à la conception du kicad de la carte''' | |||
[[Fichier:Datasheet ATMEGA8U2.pdf|gauche|194x194px|vignette|Datasheet ATMEGA8U2]] | |||
[[Fichier:AVR042.pdf|199x199px|vignette|AVR Hardware Design Considerations|centré]] | |||
<p style="clear: both;" /> | |||
=== Vue 3D === | |||
[[Fichier:3D Kicad Programmmateur AVR.png|centré|sans_cadre|521x521px|Programmmateur AVR - 3D KICAD]] | |||
<p style="clear: both;" /> | |||
=== Fichier kicad === | |||
[[Fichier:2024-PSE-G2-Prog VFinale sans erreur.zip|alt=2024-PSE-G2-Prog VFinale|centré]] | |||
<p style="clear: both;" /> | |||
=== Brasure === | |||
[[Fichier:Brasure avant carte prog avr.jpg|gauche|vignette|Carte programmateur AVR - Vue avant]] | |||
[[Fichier:Brasure arriere carte prog avr.jpg|centré|vignette|Carte programmateur AVR - Vue arrière]] | |||
<p style="clear: both;" /> | |||
=== 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 ===== | |||
<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. | |||
[[Fichier:Terminal - cmd lsusb avec carte.png|droite|sans_cadre|712x712px]] | |||
[[Fichier:Terminal - Cmd lsusb.png|gauche|sans_cadre|756x756px]] | |||
<p style="clear: both;" /> | |||
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 | |||
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 | |||
[...] | |||
</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 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;" /> | |||
[[Fichier:Led carte.mp4|500px|left|led_carte]] | |||
[[Fichier:Bouton carte.mp4|500px|right|bouton_carte]] | |||
<p style="clear: both;" /> | |||
* 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 : | |||
<syntaxhighlight lang="makefile" line="1" start="1"> | |||
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 | |||
</syntaxhighlight> | |||
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. | |||
[[Fichier: | * 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. | ||
[[Fichier: | <syntaxhighlight lang="terminfo"> | ||
sudo minicom -os | |||
</syntaxhighlight> | |||
<p style="clear: both;" />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]] | |||
<p style="clear: both;" /> | |||
Il va falloir procéder à 3 changements : | |||
# '''''Appuyer sur A''''' et changer modem par l'emplacement de notre carte (ici /ttyACM0) puis ENTRER | |||
# '''''Appuyer sur E''''' puis appuyer sur C pour configurer la vitesse de notre minicom puis ENTRER | |||
# '''''Appuyer sur F''''' puis ENTRER | |||
<p style="clear: both;" /> | <p style="clear: both;" /> | ||
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 : | |||
[[Fichier:Interface minicom vierge.png|gauche|sans_cadre|620x620px]] | |||
[[Fichier:Interface minicom apres reception message.png|centré|sans_cadre]] | |||
<p style="clear: both;" /> | |||
* Nous avons ensuite implémenté un programme permettant d'allumer une led différente à chaque bouton-poussoir pressé. | |||
<p style="clear: both;" /> | |||
[[Fichier:Lufa boutons.mp4|vignette|Demonstration vidéo de notre programme utilisant la LUFA|centré]] | |||
<p style="clear: both;" /><p style="clear: both;" /> | |||
===== <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 | |||
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(); | |||
} | |||
</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 | |||
} | |||
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'; | |||
} | |||
</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) | |||
// 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'; | |||
} | |||
</syntaxhighlight>'''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 (<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 :
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 :
É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 :
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é :
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
Comprendre notre schéma
Vue 3D
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.
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.
- 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).
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).
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.
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.
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.
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)
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é.
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 :
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 :
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 (ici22.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 :
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 :
Et voici les noeuds installés :
Maintenant regardons notre site final :
On clique sur la petite icône est nous sommes renvoyé sur cette url : http://localhost:1880/ui/
Voici notre interface finale :
Et une démonstration du projet dans son intégralité en vidéo ci dessous :
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
Conception de notre schéma électrique
Documents relatifs à la conception du kicad de la carte
Vue 3D
Fichier kicad
Fichier:2024-PSE-G2-Prog VFinale sans erreur.zip
Brasure
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).
- 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
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.
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 :
Il va falloir procéder à 3 changements :
- Appuyer sur A et changer modem par l'emplacement de notre carte (ici /ttyACM0) puis ENTRER
- Appuyer sur E puis appuyer sur C pour configurer la vitesse de notre minicom puis ENTRER
- 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 :
- Nous avons ensuite implémenté un programme permettant d'allumer une led différente à chaque bouton-poussoir pressé.
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.