bunex-industries

Projecteur laser à balayage

Poursuivant les travaux autour des projecteurs ou afficheurs laser, voici une approche "bitmap" ou "raster" du problème, c'est à dire un formation d'image par balayage ligne par ligne.

Aperçu du montage

Le montage repose sur le fonctionnement coordonné de plusieurs sous-systèmes dont voici un aperçu. L'idée étant de synchroniser un balayage horizontal très rapide avec un balayage vertical, tout en maîtrisant un stream ultra-rapide d'allumages et d'extinctions du laser.

Le balayage horizontal est assuré par ce que je pense être la pièce maîtresse de cet appareil : le miroir hexagonal tournant. Cette pièce est sans doute la trouvaille la plus intéressante lorsqu'on récupère des pièces dans une imprimante laser. Et la plus indispensable dans cette application. Je ne crois pas qu'il soit possible d'en réaliser une "from scratch" avec les moyens usuels d'un maker (en tout cas les miens).

Pour une fois, j'ai travaillé :-) sur une vidéo explicative, découvrant au passage les mérites d'Adobe Première :

Résultats

Aspects électroniques

Deux Arduino Nano ont été nécessaires pour mettre en oeuvre ce montage. Le premier est entièrement dédié au pilotage du miroir hexagonal tournant. Le deuxième s'occupe de la gestion des synchronisations horizontales et verticales (via des interruptions) et du pilotage du laser.

Les deux Arduinos et l'électronique qui leur sont associées fonctionnent en boucle fermée, c'est à dire que leurs sorties sont dépendantes des lectures sur leurs entrées.

Les moteurs brushless (sans-balais) sont étonnamment complexes à faire tourner. Les 11 fils de connexions m'ont laissé perplexe au début. Après moults recherches et essais/erreurs, voici le schéma d'un possible driver :

Le reste de l'électronique, relatif à l'arduino 2, se résume à :

Aspects logiciels

Voici le code complet des deux micro-programmes embarqués dans les Arduinos Nano.

On remarquera dans ce premier programme dédié au driver moteur la séquence d'excitation des bobines en fonction du secteur dans lequel se trouve le rotor. Des interruptions auraient pu être employées ici aussi mais je ne crois pas que cela aurai permis de mieux fonctionner. D'autant plus que le moteur atteint déjà des vitesses supérieures à celles dont nous avons besoin ici.


// BLDC Driver for motorized polygon mirror
// board with 3x L293D H-bridges

// fast ADC
#define FASTADC 1
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

// Hall sensors input pins
int HU = 2; //A
int HV = 3; //B
int HW = 4; //C

// output pins for 3 half-H-bridges
int EN1 = 5;
int IN1 = 6;
int EN2 = 7;
int IN2 = 8;
int EN3 = 9;
int IN3 = 10;
int POT = A0;
int t = 0;

void setup() {

	// this to setup maximum frequency PWM generation.
	// but PWM is made in another fashion so it's not useful anymore.
  byte mode = 0x01;
  TCCR2B = TCCR2B & 0b11111000 | mode; //3,11

  #if FASTADC
    // set prescale to 16 : ADC readings at 55 KHz => 20 µs 
    // we should get the value of the velocity potentiometer in no time.
    sbi(ADCSRA, ADPS2) ;
    cbi(ADCSRA, ADPS1) ;
    cbi(ADCSRA, ADPS0) ;
  #endif
  
  pinMode(HU, INPUT); //A
  pinMode(HV, INPUT); //B
  pinMode(HW, INPUT); //C
  pinMode(EN1, OUTPUT);
  pinMode(IN1, OUTPUT);
  pinMode(EN2, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(EN3, OUTPUT);
  pinMode(IN3, OUTPUT);
}

void loop() {

	// pseudo PWM generation
  int velocity = analogRead(POT)/64;
  int PWM = t >= velocity;
  t = (t+1)%16;
  
  	// readings from Hall effect sensors
  int sum = (!digitalRead(HW)) + 2*(!digitalRead(HV)) + 4*(!digitalRead(HU));
  
  	// coils excitation table.
  switch (sum) {
    case 0: // 000
      break;
    case 1: // 001
      digitalWrite(EN3, PWM && HIGH);
      digitalWrite(IN3, HIGH);
      digitalWrite(EN2, LOW);
      digitalWrite(IN2, LOW);
      digitalWrite(EN1, PWM && HIGH);
      digitalWrite(IN1, LOW);
      break;
    case 2: // 010
      digitalWrite(EN3, LOW);
      digitalWrite(IN3, LOW);
      digitalWrite(EN2, PWM && HIGH);
      digitalWrite(IN2, LOW);
      digitalWrite(EN1, PWM && HIGH);
      digitalWrite(IN1, HIGH);
      break;
    case 3: // 011
      digitalWrite(EN3, PWM && HIGH);
      digitalWrite(IN3, HIGH);
      digitalWrite(EN2, PWM && HIGH);
      digitalWrite(IN2, LOW);
      digitalWrite(EN1, LOW);
      digitalWrite(IN1, LOW);
      break;
    case 4: // 100
      digitalWrite(EN3, PWM && HIGH);
      digitalWrite(IN3, LOW);
      digitalWrite(EN2, PWM && HIGH);
      digitalWrite(IN2, HIGH);
      digitalWrite(EN1, LOW);
      digitalWrite(IN1, LOW);
      break;
    case 5: // 101      
      digitalWrite(EN3, LOW);
      digitalWrite(IN3, LOW);
      digitalWrite(EN2, PWM && HIGH);
      digitalWrite(IN2, HIGH);
      digitalWrite(EN1, PWM && HIGH);
      digitalWrite(IN1, LOW);
      break;
    case 6: // 110
      digitalWrite(EN3, PWM && HIGH);
      digitalWrite(IN3, LOW);
      digitalWrite(EN2, LOW);
      digitalWrite(IN2, LOW);     
      digitalWrite(EN1, PWM && HIGH);
      digitalWrite(IN1, HIGH);
      break;
    case 7: // 111
      break; 
  }
}

Voici à présent le micro-programme du second Arduino. Comme il est ici question de synchronisation à grande vitesse, les processus en jeu sont fortement "time critical". On ne peut plus simplement lire les états des entrées au cours de la boucle d'exécution principale pour connaître les tops de synchronisation car nous aurions alors toutes les chances de les manquer.

Heureusement, le micro-contrôleur Atmel 328 dont est équipé l'Arduino Nano est doté d'un système d'interruption. Les interruptions permettent d'interrompre le déroulement normale de la boucle principale lorsqu'un évènement survient sur une broche. L'Arduino exécute à la place une fonction d'interruption avant de reprendre le cours normal de la boucle.

Deux broches sont configurées pour détecter des interruptions : la première détecte le top de début d'image, la deuxième détecte le top de début de ligne. Des délais ajustables de l'ordre de la centaine de micro-seconde permettent d'ajuster finement les choses. À la fin, nous avons un micro-programme dont la boucle principale est vide de toute instruction ! Tous les processus sont gérés dans les routines d'interrupt.


const int LASER = 7;
const int PHOTODIODE = 3;
const int MIRROR = 2;

volatile int lineIndex = 0;
volatile int iterationCount = 3;
volatile int iterationIndex = 0;
volatile int shouldTrace = 0;

byte bytes[128] = {
B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
B01111111,B10000111,B10001111,B00111100,B01110001,B11111111,B01111000,B11110000,
B01111111,B11000111,B10001111,B00111100,B01110001,B11111111,B01111101,B11110000,
B01111001,B11100111,B10001111,B00111110,B01110001,B11111111,B00111101,B11100000,
B01111001,B11100111,B10001111,B00111111,B01110001,B11000000,B00011111,B11000000,
B01111111,B11000111,B10001111,B00111111,B01110001,B11000000,B00001111,B10000000,
B01111111,B11000111,B10001111,B00111111,B11110001,B11111110,B00001111,B10000000,
B01111001,B11100111,B10001111,B00111011,B11110001,B11000000,B00011111,B11000000,
B01111000,B11100111,B10001111,B00111001,B11110001,B11000000,B00011111,B11000000,
B01111011,B11100111,B11111110,B00111001,B11110001,B11111111,B00111101,B11100000,
B01111111,B11100011,B11111110,B00111000,B11110001,B11111111,B01111000,B11110000,
B01111111,B11000001,B11111100,B00111000,B01110001,B11111111,B01111000,B11110000,
B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,
B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000
};

void setup() {
  pinMode(MIRROR, INPUT);
  pinMode(PHOTODIODE, INPUT);
  attachInterrupt(digitalPinToInterrupt(MIRROR), startFrame, FALLING);
  attachInterrupt(digitalPinToInterrupt(PHOTODIODE), startLine, RISING);
  pinMode(LASER, OUTPUT);
  digitalWrite(LASER, HIGH);
}

void loop() {
	// EMPTY LOOP !!!!
}

void startLine() {
  if(shouldTrace == true) {
    digitalWriteFast(LASER, LOW);
    delayMicroseconds(150);
    for (int b = 0 ; b < 8 ; b++) {
      for (int i = 0 ; i < 8 ; i++) {
        digitalWriteFast(LASER, bitRead(bytes[lineIndex + b], 7 - i));
      }
    }
    iterationIndex = (iterationIndex + 1) % iterationCount;
    lineIndex = (lineIndex + 8 * (iterationIndex == 0));
    if(lineIndex == 128) {
      shouldTrace = false;
      digitalWriteFast(LASER, LOW);
      return;
    }
    digitalWriteFast(LASER, HIGH);
  }
}

void startFrame() {

  int mils = 4;
  for(int ms = 0 ; ms < mils ; ms++) {
    delayMicroseconds(1000);
  }
  lineIndex = 0;
  iterationIndex = 0;
  shouldTrace = true;
  digitalWriteFast(LASER, HIGH);
}

void digitalWriteFast(uint8_t pin, uint8_t x) {
    PORTD ^= (-x ^ PORTD) & (1 << pin);
}

Aspects optiques

Dès qu'il est question de miroir pour des appareils d'optiques, les miroirs ordinaires ne sont que peu adaptés. En effet, la lame de verre qui recouvre la couche réfléchissante reflète également la lumière et crée une double réflexion. On utilise des miroirs "first surface", dépourvus de cette lame de verre. Heureusement, on en trouve dans les imprimantes laser. Le miroir de déflexion vertical est l'un de ces miroirs.

Les facettes réfléchissantes du miroir hexagonal tournant sont également des first surface mirrors. Le balayage créé par ce miroir est assez intuitif, mais à toutes fins utiles, voici une petite animation en canvas HTML :

Le miroir possède 6 facettes, nous aurons donc 6 balayage par tour. Les facettes sont usinées très précisément de manière à obtenir des balayages identiques (superposables). Le calcul (mais simplement l'observation) montre que le balayage se fait sur 120°.

Si le cône de projection est si "ouvert", l'image projetée va très vite grandir avec la distance (la taille de l'image triple avec la distance). Ceci est plutôt un inconvénient en pratique. Il convient de resserrer cet angle de projection. On utilise à cette fin une lentille convergente. Cependant cette lentille va introduire une convergence indésirable sur le faisceau laser lui-même (qui n'est pas infiniment fin). Cette seule lentille corrige donc bien notre problème de géométrie mais entraine une image floue. Une lentille divergente, en amont sur le chemin du laser, va donc pré-diverger le faisceau :

Effet d'une lentille convergente sur des faisceaux "théoriques" sans épaisseur : l'image est resserrée.


Situation en réalité : le faisceau (constitué de rayons parallèles avant de frapper la lentille) se met à converger. Les rayons se croisent rapidement puis divergent. Le spot laser sur l'écran sera flou.


En plaçant une lentille divergente avant le miroir tournant, on introduit une divergence pour la compenser. La position de cette lentille va d'ailleurs permettre un réglage de mise-au-point.


Performances

Pour se faire une idée des performances requises, faisons un rapide calcul :

L'état du laser doit donc être mis à jour toutes les 4 microsecondes. Ceci est sans doute la dernière limite accessible avec un arduino. Et encore, il est absolument nécessaire d'updater la broche de sortie via la manipulation des registres de l'Atmel, les commandes de digitalWrite() ordinaires étant bien trop lentes.