Micro contrôleurs AVR/Travail pratique/Utilisation du shield LCD keypad
Dans ce TP, nous allons utiliser le shield LCD keypad permettant d'afficher sur un LCD de deux lignes de 16 caractères. L'objectif du TP est de réaliser un réveil. Toute la programmation se fera en C et ne nécessitera aucune librairie extérieure.
Gestion du LCD
modifierLa gestion du LCD se fera à l'aide d'une librairie de très bas niveau écrite par Michel Doussot à laquelle nous avons ajouté quelques fonctions et surtout que nous avons adapté pour l'Arduino UNO.
Voici la librairie :
// lcd.c
//
// Copyright 2014 Michel Doussot <michel@mustafar>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
// MA 02110-1301, USA.
#include <avr/io.h>
#define RS 0x01
#define E 0x02
#define DATAS 0xF0
// unite approximative 2us
void delai(unsigned long int delai) {
volatile long int i=0;
for(i=0;i<delai;i+=1);
}
// Arduino UNO
//PORTD : b7 b6 b5 b4
// D3 D2 D1 D0
// PORTB :b1 b0
// E RS
void rs_haut(void) {
PORTB = PORTB | RS;
}
void rs_bas(void) {
PORTB = PORTB & ~RS;
}
void e_haut(void) {
PORTB = PORTB | E;
delai(8);
}
void e_bas(void) {
PORTB = PORTB & ~E;
delai(8);
}
void e_puls(void) {
e_haut();
e_bas();
}
void ecris_4(unsigned char valeur) {
unsigned char v;
v = (valeur << 4) & DATAS;
PORTD = PORTD & ~DATAS ;
PORTD = PORTD | v ;
e_puls();
}
void ecris_8(unsigned char valeur) {
unsigned char v;
v = valeur & DATAS;
PORTD = PORTD & ~DATAS ;
PORTD = PORTD | v ;
e_puls();
v = (valeur << 4) & DATAS;
PORTD = PORTD & ~DATAS ;
PORTD = PORTD | v ;
e_puls();
}
void setup() {
PORTB = 0;
delai(6000);
ecris_4(0x03);
delai(1600);
ecris_4(0x03);
delai(800);
ecris_4(0x03);
delai(800);
ecris_4(0x02);
delai(40);
ecris_4(0x02);
ecris_4(0x08);
delai(40);
ecris_4(0x00);
ecris_4(0x06);
delai(40);
ecris_4(0x00);
ecris_4(0x0C);
delai(40);
ecris_4(0x00);
ecris_4(0x01);
delai(800);
}
void writecar(char car) {
rs_haut();
ecris_8((unsigned char)car);
}
void writestr(char *chaine) {
rs_haut();
while (*chaine) {
ecris_8((unsigned char)*chaine++);
}
}
void command (uint8_t value) {
rs_bas();
ecris_8(value);
}
#define LCD_CLEARDISPLAY 0x01
void clearScreen() {
command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero
delai(1000); // this command takes a long time!
}
#define LCD_SETDDRAMADDR 0x80
void setCursor(uint8_t col, uint8_t row)
{
col = col & 0x0F; // %16
row = row & 0x01; // %2
command(LCD_SETDDRAMADDR | (col + 0x40*row));
}
Exercice 1
modifierÉcrire un programme d'exemple d'utilisation de cette librairie.
int main(void) {
unsigned char tmp;
DDRD = 0xF0;
DDRB = 0x03;
setup();
clearScreen();
tmp = 0;
writestr("Hello Microsoft ");
setCursor(2,1);
writestr("Hello Linux");
while (1) {
//Ce qui est fait dans cette boucle infinie n'a aucune espèce d'importance
delai(100000);
tmp += 1;
}
return 0;
}
Ce programme fonctionne si la librairie donnée est ajoutée avant le main.
Ajouter le code de la librairie de départ avant le main se fera systématiquement dans toute la suite de ce TP même si cela n'est pas rappelé.
Tous les sous-programmes qui vous seront demandés dans la suite auront la même destination.
Exercice 2
modifierÉcrire un sous-programme capable d'afficher un nombre de 8 bits en décimal sur l'afficheur LCD.
void afficheNb8bitsBinaire(uint8_t nb) {
writecar(nb / 100 + '0');
writecar(nb / 10 % 10 + '0');
writecar(nb % 10 + '0');
}
Un exemple d'utilisation est :
int main(void) {
DDRD = 0xF0;
DDRB = 0x03;
setup();
afficheNb8bitsBinaire(254);
setCursor(2,1);
writestr("Hello Linux");
while (1);
return 0;
}
Exercice 3
modifierÉcrire un sous-programme capable d'afficher un nombre de 8 bits, déjà en format BCD, en décimal sur l'afficheur LCD.
void afficheNb8bitsBCD(uint8_t nb) {
writecar(((nb & 0xF0)>>4) + '0');
writecar((nb & 0x0F) + '0');
}
Un exemple d'utilisation est :
int main(void) {
DDRD = 0xF0;
DDRB = 0x03;
setup();
afficheNb8bitsBCD(0x78);
setCursor(2,1);
writestr("Hello Linux");
while (1);
return 0;
}
qui affichera 78 sur l'écran
Exercice 4
modifierOn vous donne un sous-programme capable de gérer l'incrémentation d'une variable 16 bits en format représentant des heures et minutes.
void incrementHHMM(uint16_t *hh_mm) {
(*hh_mm)++;
if ((*hh_mm & 0x000F) > 0x0009)
*hh_mm += 0x0006;
if ((*hh_mm & 0x00F0) > 0x0050)
*hh_mm += 0x00A0;
if ((*hh_mm & 0x0F00) > 0x0900)
*hh_mm += 0x0600;
if ((*hh_mm & 0xFF00) > 0x2300)
*hh_mm = 0x0000;
}
Essayer ce sous programme et réaliser la décrémentation des heures et minutes sur le même principe. Préparer alors un programme d'affichage des heures et minutes pour un essai.
void decrementHHMM(uint16_t *hh_mm) {
(*hh_mm)--;
if ((*hh_mm & 0x000F) == 0x000F)
*hh_mm -= 0x0006;
if ((*hh_mm & 0x00F0) == 0x00F0)
*hh_mm -= 0x00A0;
if ((*hh_mm & 0x0F00) == 0x0F00)
*hh_mm -= 0x0600;
if ((*hh_mm & 0xFFFF) > 0x2359)
*hh_mm = 0x2359;
}
void afficheHHMM(uint16_t hh_mm) {
writecar(((hh_mm & 0xF000)>>12) + '0');
writecar(((hh_mm & 0x0F00)>>8) + '0');
writecar(':');
writecar(((hh_mm & 0x00F0)>>4) + '0');
writecar((hh_mm & 0x000F) + '0');
}
Un exemple d'utilisation est :
int main(void) {
uint16_t HH_mm = 0;// pour les heures et minutes
DDRD = 0xF0;
DDRB = 0x03;
setup();
setCursor(2,1);
writestr("Hello Linux");
while (1) {
incrementHHMM(&HH_mm);// ou decrement
setCursor(2,0);
afficheHHMM(HH_mm);
delai(100000);
}
return 0;
}
Ressources
modifierPour ceux qui désireraient une variation par rapport aux exercices proposés nous proposons quelques sous-programmes à essayer.
Il s'agit d'une incrémentation et décrémentation BCD sur 4 digits (donc 16 bits) :
void incrementBCD(uint16_t *cnt) {
(*cnt)++;
if ((*cnt & 0x000F) > 0x0009) *cnt += 6;
if ((*cnt & 0x00F0) > 0x0090) *cnt += 0x0060;
if ((*cnt & 0x00F0) > 0x0900) *cnt += 0x0600;
if ((*cnt & 0x00F0) > 0x9000) *cnt += 0x6000;
}
void decrementBCD(uint16_t *cnt) {
(*cnt)--;
if ((*cnt & 0x000F) == 0x000F) *cnt -= 6;
if ((*cnt & 0x00F0) == 0x00F0) *cnt -= 0x0060;
if ((*cnt & 0x0F00) == 0x0F00) *cnt -= 0x0600;
if ((*cnt & 0xF000) == 0xF000) *cnt -= 0x6000;
}
Gestion de la conversion analogique numérique
modifierLa particularité de ce shield est de proposer une gestion des boutons avec une seule broche de l'ATMega328. Pour réaliser cela, il faut naturellement utiliser une conversion analogique numérique comme nous l'avons déjà présenté dans le chapitre correspondant.
Exercice 5
modifierOn vous demande de préparer un sous-programme capable d'afficher le résultat de la conversion analogique numérique (CAN dans la suite).
Indication : on rappelle que la CAN est sur 10 bits et peut donc retourner un nombre entre 0 et 1023.
L'indication ci-dessus nous explique que pour un nombre sur 10 bits sa valeur décimale peut varier entre 0 et 1023 ce qui fait 4 digits. Il faut donc prévois l'affichage de 4 chiffres sur le LCD.
void afficheNb10bitsCAN(uint16_t nb) {
writecar(nb / 1000 + '0');
writecar(((nb / 100) % 10) + '0');
writecar(nb / 10 % 10 + '0');
writecar(nb % 10 + '0');
}
Exercice 6
modifierRéaliser la conversion analogique numérique en cherchant comment sont montés les boutons poussoirs. Utiliser le résultat de l'exercice 5 pour faire vos tests. Ce travail doit être séparé en deux sous-programmes :
- un sous programme de configuration du CAN
- un sous-programme de lecture du CAN proprement dit
* Voici le programme de configuration du CAN
void setupCANForKeypad() {
// division horloge par 128
ADCSRA |= ((1<<ADPS0) | (1<<ADPS1) | (1<<ADPS2));
// autorisation horloge
ADCSRA |= (1<<ADEN);
// choix de AVCC comme référence
ADMUX |= (1<<REFS0);
}
- Voici maintenant le sous programme de lecture du CAN
uint16_t readCANForKeypad() {
// on lance la conversion
ADCSRA |= (1 << ADSC);
// on attend qu'elle soit finie
while ((ADCSRA & (1 << ADSC))==(1 << ADSC));
return ADC;
}
- Voici maintenant un programme d'utilisation de tout cela avec affichage de la valeur du CAN
#include <avr/io.h>
int main(void) {
uint16_t HH_mm = 0;
DDRD = 0xF0;
DDRB = 0x03;
setup();
setupCANForKeypad();
while (1) {
incrementHHMM(&HH_mm);
setCursor(2,0);
afficheHHMM(HH_mm);
// affichage du resultat du CAN
setCursor(2,1);
afficheNb10bitsCAN(readCANForKeypad());
delai(100000);
}
return 0;
}
Exercice 7
modifierUtiliser l'expérimentation de l'exercice 6 pour écrire un programme qui affiche en clair sur le LCD le bouton parmi {SELECT,LEFT,RIGHT,UP,DOWN} qui a été appuyé.
Indications :
- Évidemment ce travail se fait à partir des résultats obtenus dans l'exercice 6. En effet il est possible de calculer des seuils de tension entre les boutons.
- On vous propose d'abord de réaliser une fonction qui retourne un uint8_t avec les valeurs suivantes :
- pas de touche appuyée : valeur 0,
- touche LEFT appuyée, mettre b0 à 1
- touche SELECT appuyée mettre b1 à 1
- touche UP appuyée mettre b2 à 1
- touche DOWN appuyée mettre b3 à 1
- touche RIGHT appuyée mettre b4 à 1
Son prototype est :
uint8_t ComputeTouch();
et elle retourne l'information d'appui comme ci-dessous, dans une variable 8 bits :
b7
|
b6
|
b5
|
b4
|
b3
|
b2
|
b1
|
b0
|
0
|
0
|
0
|
RIGHT
|
DOWN
|
UP
|
SELECT
|
LEFT
|
- Réaliser ensuite un sous-programme responsable d'afficher le nom de la touche appuyée.
// retourne une valeur au format :
// b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0
// 0 | 0 | 0 | Right | Down | Up | Select | Left
uint8_t ComputeTouch() {
int16_t can;
int8_t res;
can = readCANForKeypad();
if (can >870) res=0; else
if (can >600) res =2; else
if (can > 360) res=1; else
if (can > 220) res=8; else
if (can >75) res=4; else
res=16;
return res;
}
// pourrait etre aussi realise avec un case
void afficheTouch() {
int8_t touche;
touche = ComputeTouch();
setCursor(2,1);
if (touche == 0) writestr(" ");
if (touche == 1) writestr(" LEFT");
if (touche == 2) writestr("SELECT");
if (touche == 4) writestr(" UP");
if (touche == 8) writestr(" DOWN");
if (touche == 16) writestr(" RIGHT");
}
int main(void) {
uint16_t HH_mm = 0;
DDRD = 0xF0;
DDRB = 0x03;
setup();
setupCANForKeypad();
while (1) {
incrementHHMM(&HH_mm);
setCursor(2,0);
afficheHHMM(HH_mm);
// affichage du resultat du CAN
afficheTouch();
delai(100000);
}
return 0;
}
Gestion d'un réveil
modifierNous allons maintenant utiliser les sous-programmes que l'on a réalisé précédemment pour réaliser un réveil. Nous allons commencer par le réglage de l'heure de réveil avec les boutons.
Exercice 8
modifierRéaliser un réglage de l'heure de réveil avec les deux boutons "UP" et "DOWN".
Indications :
- une vitesse lente peut être réalisée en détectant un appui sur un bouton "UP" ou "DOWN"
- une vitesse plus lente de défilement put être réalisée en restant appuyé sur "UP" ou "DOWN"
- option : envisager un incrément qui augmente en fonction du temps d'appui
Voici une solution partielle :
On commence par le sous-programme responsable de l'affichage de l'heure réveil.
void AfficheHeureReveil(uint16_t HH_MMR) {
setCursor(2,1);
afficheHHMM(HH_MMR);
}
On continue ensuite pour afficher l'heure courante et l'heure réveil en même temps ainsi que la possibilité de régler l'heure réveil.
int main(void) {
uint16_t HH_mm = 0,HH_MM_Reveil=0;
uint8_t touch_old, touch_present;
DDRD = 0xF0;
DDRB = 0x03;
setup();
setupCANForKeypad();
AfficheHeureReveil(HH_MM_Reveil);
while (1) {
incrementHHMM(&HH_mm);
// affichage heure courante
setCursor(2,0);
afficheHHMM(HH_mm);
touch_present = ComputeTouch();
if ((touch_present == 4) && (touch_old==0)) {
incrementHHMM(&HH_MM_Reveil);
AfficheHeureReveil(HH_MM_Reveil);
}
if ((touch_present == 4) && (touch_old==4)) {
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
AfficheHeureReveil(HH_MM_Reveil);
}
if ((touch_present == 8) && (touch_old==0)) {
// compléter pour gestion du décrément !!!
}
// gérer le décrément rapide ici !!!!
delai(100000);
touch_old = touch_present;
}
return 0;
}
Exercice 9
modifierRéaliser un mécanisme d'armement du réveil (avec la touche "SELECT") lui permettant de sonner quand l'heure courante est égale à l'heure réveil.
Indications : comme le montre le dessin, la gestion d'un mécanisme de gestion de sonnerie peut se faire avec 3 états. La terminologie du dessin empruntée d'un wikibook sur VHDL est assez différente de celle de la présente section :
- KEY sera à remplacer par SELECT
- TRIP sera l'égalité entre l'heure courante et l'heure réveil (qui ne dure qu'une minute)
- L'action RING <= '1' déclenche la sonnerie
- On pourra utiliser un type enum pour gérer l'automatisme du réveil :
enum etats {Off,Armed,Ringing}; // declaration du type énuméré
//......
enum etats etat=Off; // declaration et initialisation
int main(void) {
uint16_t HH_mm = 0,HH_MM_Reveil=0;
uint8_t touch_old, touch_present, select=0,egalite=0;
enum etats etat=Off;
DDRD = 0xF0;
DDRB = 0x03;
setup();
setupCANForKeypad();
//writestr("Hello Microsoft ");
//afficheNb8bitsBinaire(tmp);
//writecar(' ');
//afficheNb8bitsBCD(0x79);
//setCursor(2,1);
//writestr("Hello Linux");
while (1) {
incrementHHMM(&HH_mm);
setCursor(2,0);
afficheHHMM(HH_mm);
touch_present = ComputeTouch();
if ((touch_present == 4) && (touch_old==0)) {
incrementHHMM(&HH_MM_Reveil);
AfficheHeureReveil(HH_MM_Reveil);
}
if ((touch_present == 4) && (touch_old==4)) {
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
incrementHHMM(&HH_MM_Reveil);
AfficheHeureReveil(HH_MM_Reveil);
}
if ((touch_present == 8) && (touch_old==0)) {
decrementHHMM(&HH_MM_Reveil);
AfficheHeureReveil(HH_MM_Reveil);
}
if ((touch_present == 8) && (touch_old==8)) {
decrementHHMM(&HH_MM_Reveil);
decrementHHMM(&HH_MM_Reveil);
decrementHHMM(&HH_MM_Reveil);
decrementHHMM(&HH_MM_Reveil);
decrementHHMM(&HH_MM_Reveil);
AfficheHeureReveil(HH_MM_Reveil);
}
// machine d'etas de la sonnerie
//Preparation des entrees
if ((touch_present == 2) && (touch_old==0)) //SELECT
select=1;
else
select=0;
if (HH_mm == HH_MM_Reveil) egalite=1; else egalite=0;
//graphe d'évolution
switch (etat) {
case Off: if (select) etat = Armed; break; //OFF->Armed
case Armed: if (select) etat = Off;else if (egalite) etat = Ringing;else etat=Armed;break; //Armed->Ring
case Ringing: if(select) etat = Off; break;
default: etat=Off;
}
// Gestion des sorties
setCursor(8,0);
if (etat) writecar('@'); else writecar(' ');
if (etat==Ringing) writecar(0xFF); else writecar(' ');
delai(100000);
touch_old = touch_present;
}
return 0;
}