Bracelet haptique à effet peltier

La plupart des interfaces haptiques se basent sur un dispositif électromécanique (moteur miniature et masse décentrée) afin de produire une vibration.Bien que le niveau d’intensité puisse être contrôlé, la plage dynamique dans les sensations très faibles reste difficile à atteindre. De plus, la vibration produit également un son, ce qui peut interférer avec une interface auditive, ou déconcentrer l’usager.

Dans cette exploration technologique, je me demande quels sont les possibilités qu’offrent les nouveaux mini modules thermoélectriques disponibles comercialement. Je me suis concentré sur deux pièces fabriquées par CUI, le CP2088-219 et le CP0734-238. Le premier fait 8x8mm de large, et le second, 3,4×3,4mm (!). La raison derrière mon choix est la précision spatiale, la faible consommation électrique et la facilité d’intégration dans un assemblage complètement portatif.

L’une de mes idées plus générales était de se servir de ce bracelet comme d’un facilitateur empathique à la limite du conscient, en reproduisant en temps réel une mesure physiologique provenant d’une autre personne. J’en parlerai plus en détail dans la conclusion. Comme d’habitude, toutes les bonnes idées manquent d’originalité et ont déjà été brevetées (https://patents.google.com/patent/WO2013058886A1), mais c’est pas ce qui va m’empêcher de faire des recherches dans ce domaine.

Heatsink

Afin d’optimiser le transfert thermique (Q et dQ/dt), un heatsink doit être attaché sur la face chaude du peltier. Comme je ne savais pas trop ce que je faisais, j’ai choisi d’y aller avec la simplicité : un heatsink en alu avec des ailettes super larges et espacées, pour favioriser un refroidissement passif. J’ai aussi exploré un micro blower fan, mais je ne lui ai pas trouvé de heatsink compatible. Bref, il y a une grande différence entre faire un cours de transfert thermique théorique, et être confronté à la jungle des pièces disparates en pratique. Je comprends les concepteurs de fonctionner avec des assemblages déjà faits, ou bien de concevoir des pièces sur mesure. En outre, je n’ai pas trouvé de distributeur qui rend cela clair. Bref, après en avoir acheté plusieurs un peu au hasard, je me suis arrêté sur celui-ci : ATS-52150G-C1-R0. Il fait 15x15mm de large et a une résistance thermique de 11,7­ degC/W. Étant donné que mon utilisation est avec un rapport cyclique très faible (le peltier est pulsé et beaucoup plus souvent à off qu’à on), j’ai estimé que cela serait bon.

Estimation du seuil de sensibilité et du seuil de douleur

En utilisant une source de courant ajustable entre 0 et 2A, et entre 0 et 4V et le peltier CP2088 en contact direct avec l’index du côté froid, et le heatsink susmentionné du côté chaud, j’ai pu produire des transferts thermiques entre 0 et 4W, pour des durées de 1 à 9 secondes avec le montage électronique que je présenterai plus tard. Le seuil de sensibilité (mesure très subjective) est un peu en-dessous de 0,5W. Le seuil de douleur était autour de 3W. Enocre une fois, c’est subjectif. La sensation de douleur ne provient pas d’une brûlure réelle (surtout que le peltier refroidit), mais du gradient super élevé de changement de température, qui affole les capteurs physiologiques. Passer par exemple de 25 degrés à 20 degrés en une fraction de seconde semble simuler le contact subit avec un objet extrêmement froid, d’où la sensation de douleur et le réflexe de vouloir retirer son doigt. Je ne connais pas bien les mécanismes de reset biologiques, mais la sensation perdure plusieurs secondes, voire minutes, après que la puissance électrique soit retirée du peltier, un peu à la manière de la persistence rétienne, où un flash de lumière reste dans le champ visuel.

Ce que j’ai remarqué, c’est que les capteurs de température du corps sont beaucoup plus sensibles au changement de température qu’à la valeur absolue. Une température de 20 ou de 15 degrés ne produit que peu de sensation, et avec le peu de puissance de mon peltier, je ne peux descendre beaucoup plus bas, considérant la puissance thermique que fournit le corps et l’efficacité de mon heatsink. De plus, puisque mon système est portatif et à batterie, la consommation électrique doit être considérée. Utiliser le peltier dans un régime pulsé, bien qu’étant déconseillé la plupart du temps, est ici une avenue intéressante pour augmenter le ratio sensation / consommation électrique, métrique réelle d’efficacité de mon système.

J’ai aussi fait un test rapide avec le CP0734 pour voir s’il était possible de miniaturiser davantage. Hélas, son Qmax est limité à 0,2W. Cela est compensé par sa taille plus petite, mais tout de même. Je pense qu’il y a aussi une limite de surface pour les capteurs de température dans la peau, à investiguer. Bref, je l’opérais toujours au maximum de puissance sans rien sentir. Le seul endroit où la sensation était vive était sur le côté du petit doigt, à l’approche de la paume. Ce côté de la main, la tranche extérieure, est la région la plus sensible que j’aie pu trouver, du bout des doigts au coude. On pourrait penser à intégrer un tel peltier dans une bague, mais cela demanderait une miniaturisation poussée du reste de l’électronique.

Électronique de contrôle

Bien que mon intuition initiale était de simplement brancher le peltier sur un PWM, cela allait à l’encontre de tout ce qui se fait. J’ai lu des forums entiers expliquant pourquoi c’était une idée stupide de faire cela (réponse courte : la dissipation thermique va en RI^2 tandis que le transfert entre les deux faces est linéairement proportionnelle au courant. Et la chaleur produite par le peltier lui-même se rajoute au système.) Tout cela est bien beau dans les faits, mais la réalité de mon design est que j’uilise une batterie lithium et un microconrtôleur à niveau logique de 3,3V, et que je dois allumer et éteindre un courant de près de 2A. L’électronique pouvant accomplir tout cela existe, mais elle a de sérieuses limitations.

En outre, j’ai cherché un petit moment un mosfet pouvant être activé par aussi peu que 3,3V (Vgs) et fournir une résistance Rds dans les milliohms, afin de permettre un courant de 2A dans le peltier qui fait 1,43 ohm avec seulement 3,7V en entrée. Mon choix s’est porté sur le SUP40012EL-GE3.

J’aurais pu utiliser un convertisseur DC-DC pour augmenter le voltage et utiliser un mosfet ordinaire. Mais cela complexifiait déjà le circuit, pour peu d’avantages. J’ai décidé de me concentrer à avoir un premier prototype fonctionnel, et voir par la suite comment je pourrais optimiser la consommation électrique.

Dans mon exploration, j’ai voulu faire justement un convertisseur DC-DC pour pouvoir ajuster le voltage à l’entrée du peltier entre 0 et 3,7V (dans le but de contrôler le courant et donc le transfert thermique). Je me suis rendu compte (j’aurais dû lire Art of electronics pour le savoir) que les mosfets demandent un courant important pour être allumés à haute fréquence (à cause de leur capacitance élevée de gate). Et que généralement, les mosfets optimisés pour avoir une résistance basse à l’état allumé ont également le compromis d’avoir une capacitance élevée. Dans le design d’un convertisseur DC-DC, plus la fréquence est basse, plus l’inductance doit être grosse, ce qui rendait le circuit beaucoup trop massif en pratique. J’ai donc abandonné cette idée.

Mon autre idée était d’utiliser une supercapacitance de 1F pour filtrer la montée de courant et ainsi limiter l’impact sur le peltier. C’était oublier que le mosfet a un diode intégréeà l’intérieur, ce qui transforme le circuit en un charge pump qui double le voltage d’entrée. Étant donné que le peltier a une limite à 3,8V, je devais faire attention avec ma source, et cela rendait l’usage de ma batterie impossible.

Clairement, j’était encore dans une situation d’overdesign, dans le genre de keep it simple. Parfois, trop de doutes et de soucis ralentissent le développement. Je suis donc revenu à mon idée de départ, d’avoir un simple Mosfet avec un PWM à basse fréquence. Au moins, tous ces détours m’on permis de bien comprendre les limites du design et de bien choisir les composants et les paramètres. La gate du mosfet est branchée sur une pin du microcontrôleur à travers une résistance de 100 ohms, et le peltier est branché entre la batterie et le drain du mosfet. La source est reliée au ground.

Le microcontrôleur utilisé est un Feather nrf52840 Express d’Adafruit. J’utilise également une carte de prototypage faite pour les Feather (un proto shield). Le nrf52840 se programme directement avec l’IDE d’Arduino avec toutes les libraires d’Adafruit (une mine d’or). Cela a fait que le bluetooth marchait out of the box, une première dans ma vie (j’ai trop perdu de temps avec des cartes de développement obscures de Nordik, il fallait juste que j’attende qu’Adafruit fasse la grosse job de bras comme il faut).

Programmes

Le programme arduino est le suivant :

/*********************************************************************
 This is an example for our nRF52 based Bluefruit LE modules

 Pick one up today in the adafruit shop!

 Adafruit invests time and resources providing this open source code,
 please support Adafruit and open-source hardware by purchasing
 products from Adafruit!

 MIT license, check LICENSE for more information
 All text above, and the splash screen below must be included in
 any redistribution
*********************************************************************/
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>

// BLE Service
BLEDfu  bledfu;  // OTA DFU service
BLEDis  bledis;  // device information
BLEUart bleuart; // uart over ble
BLEBas  blebas;  // battery

const byte numChars = 16;
char receivedChars[numChars]; // an array to store the received data
boolean newData = false;
int peltierpin = 6;

void setup()
{
  pinMode(peltierpin, OUTPUT);
  digitalWrite(peltierpin, LOW);
  analogWriteResolution(15); //Met la fréquence du PWM à 514kHz, avec 5 bits de résolution (0-31)
  //analogWrite(peltierpin, 16384);
  Serial.begin(115200);

#if CFG_DEBUG
  // Blocking wait for connection when debug mode is enabled via IDE
  while ( !Serial ) yield();
#endif
  
  Serial.println("Bluefruit52 BLEUART Example");
  Serial.println("---------------------------\n");

  // Setup the BLE LED to be enabled on CONNECT
  // Note: This is actually the default behavior, but provided
  // here in case you want to control this LED manually via PIN 19
  Bluefruit.autoConnLed(true);

  // Config the peripheral connection with maximum bandwidth 
  // more SRAM required by SoftDevice
  // Note: All config***() function must be called before begin()
  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);

  Bluefruit.begin();
  Bluefruit.setTxPower(4);    // Check bluefruit.h for supported values
  //Bluefruit.setName(getMcuUniqueID()); // useful testing with multiple central connections
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  // To be consistent OTA DFU should be added first if it exists
  bledfu.begin();

  // Configure and Start Device Information Service
  bledis.setManufacturer("Adafruit Industries");
  bledis.setModel("Bluefruit Feather52");
  bledis.begin();

  // Configure and Start BLE Uart Service
  bleuart.begin();

  // Start BLE Battery Service
  blebas.begin();
  blebas.write(100);

  // Set up and start advertising
  startAdv();

  Serial.println("Please use Adafruit's Bluefruit LE app to connect in UART mode");
  Serial.println("Once connected, enter character(s) that you wish to send");
}

void startAdv(void)
{
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();

  // Include bleuart 128-bit uuid
  Bluefruit.Advertising.addService(bleuart);

  // Secondary Scan Response packet (optional)
  // Since there is no room for 'Name' in Advertising packet
  Bluefruit.ScanResponse.addName();
  
  /* Start Advertising
   * - Enable auto advertising if disconnected
   * - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
   * - Timeout for fast mode is 30 seconds
   * - Start(timeout) with timeout = 0 will advertise forever (until connected)
   * 
   * For recommended advertising interval
   * https://developer.apple.com/library/content/qa/qa1931/_index.html   
   */
  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(32, 244);    // in unit of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);      // number of seconds in fast mode
  Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds  
}

void loop()
{
  pollbleuart();
  updatePeltier();
}

void pollbleuart() {
  static byte ndx = 0;
  char endMarker = '\n';
  char rc;
  
  while (bleuart.available() > 0 && newData == false) {
    rc = bleuart.read();

    if (rc != endMarker) {
      receivedChars[ndx] = rc;
      ndx++;
      if (ndx >= numChars) {
        ndx = numChars - 1;
      }
    }
    else {
      receivedChars[ndx] = '\0'; // terminate the string
      ndx = 0;
      newData = true;
    }
  }
}

void updatePeltier() {
  if (newData == true) {
    uint16_t intensity = uint16_t(receivedChars[0]-48); //Temps où le Peltier est allumé en millisecondes
    if ((intensity < 1) || (intensity > 8)) intensity = 0; //protection de calculs bizarres
    analogWrite(peltierpin, 4095 * intensity);
    delay(2000); //Le pulse dure 2 secondes
    analogWrite(peltierpin, 0);    
    newData = false;
  }
}

// callback invoked when central connects
void connect_callback(uint16_t conn_handle)
{
  // Get the reference to current connection
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

  Serial.print("Connected to ");
  Serial.println(central_name);
}

/**
 * Callback invoked when a connection is dropped
 * @param conn_handle connection where this event happens
 * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
 */
void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
  (void) conn_handle;
  (void) reason;

  Serial.println();
  Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX);
}

En bref, après avoir connecté avec le PC ou tout autre contrôleur, il attend une string composée d’un chiffre de 1 à 8, qui correspond à l’intensité du PWM. La durée du pulse de froid est prédéfinie à 2 secondes. La fréquence du PWM est fixée à 125Hz en prenant la résolution la plus élevée disponible (15 bits) et en la divisant par 4096 pour obtenir seulement 3 bits. (La fréquence de la clock du PWM est de 16Mhz). Dans mes expériences, la résolution sur la sensibilité au gradient de température n’est pas beaucoup plus grande que des entiers entre 0 à 10, d’où le 8 bits. Une fréquence aussi basse permet au Mosfet de suivre facilement avec un courant à sa gate limité à 1mA.

Le code python est tout aussi facile d’utilisation grâce à l’excellent travail d’Adafruit :

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService

ble = BLERadio()

uart_connection = None

while True:
    if not uart_connection:
        print("Trying to connect...")
        for adv in ble.start_scan(ProvideServicesAdvertisement):
            if UARTService in adv.services:
                uart_connection = ble.connect(adv)
                print("Connected")
                break
        ble.stop_scan()

    if uart_connection and uart_connection.connected:
        uart_service = uart_connection[UARTService]
        while uart_connection.connected:
            s = input("Delay in s : ")
            uart_service.write(s.encode("utf-8"))
            uart_service.write(b'\n')

J’attends la connexion, puis j’ai mis un input qui envoie la string au microcontrôleur. Dans le code, c’est encore écrit délai, parce qu’au départ, c’était le temps en secondes que j’envoyais dans mes premiers tests.

Plage d’opération

Comme je l’ai dit, il y a 8 intensités de PWM. Il y a plusieurs non linéarités dans l’histoire, mais grosso-modo, il s’agit des plages de 0 à 4W de transfert thermique, avec une résolution de 0,5W (seuil de sensibilité déterminé subjectivement). La non linéarité vient du peltier et aussi peut-être des capteurs de température dans la peau. Toujours est-il que la différence entre 1 et 2 est beaucoup plus marquée qu’entre 7 et 8, cela pourrait être ajusté ultérieurement.

Pour ce qui est de la durée de 2 secondes, elle aussi a été établie empiriquement. La constante de temps du système thermique a été évaluée à environ 2-3 secondes (elle dépend un peu du courant entrée). Cela veut dire que pour une impulsion initale avec une pente infinie, la température (c’était la variable que je mesurais) prenait entre 2 et 3 secondes à atteindre sa valeur. Utiliser une durée de temps plus petite ne produit donc pas une distinction entre les pulses (c’était ce que je voulais évaluer au départ). Et utiliser une durée plus grande ne produit pas vraiment plus de sensation, puisque les transferts thermiques se stabilisent et que la peau s’habitue au contact d’un objet plus froid. Pour cette construction précise, j’ai donc optimisé la durée du pulse à 2s. En utilisant un rapport cyclique de 50%, cela veut dire que la période maximale de l’information qui peut être transmise est de 4s, ou 15bpm. Cela est très lent et empêche son utilisation pour transmettre le rythme cardiaque comme je le souhaitais au départ. Pour la respiration, c’est aussi très limite.

CAD et bracelet

Le code openSCAD pour le bracelet est le suivant :

$fn=50;
difference(){
    translate([0,-(40-23)/2,0]) cube([50,40,2]);
    translate([15,7]) cube([10,15,2]);
    translate([(50-30)/2,-6,0]) cube([30,3,2]);
    translate([(50-30)/2,23+3,0]) cube([30,3,2]);
}

translate([5/2,5/2,-13]) difference() {
    cylinder(d=5,h=13);
    cylinder(d=1.9,h=13);
}
translate([50-5/2,5/2,-13]) difference() {
    cylinder(d=5,h=13);
    cylinder(d=1.9,h=13);
}
translate([50-5/2,23-5/2,-13]) difference() {
    cylinder(d=5,h=13);
    cylinder(d=1.9,h=13);
}
translate([5/2,23-5/2,-13]) difference() {
    cylinder(d=5,h=13);
    cylinder(d=1.9,h=13);
}

La fente principale sert à mettre le peltier en contact direct avec la peau. Les deux fentes sur le côté servent à attacher le bracelet avec une bande de tissu cousue avec du velcro.

Perspectives futures

La miniaturisation va être difficile. C’est déjà un exploit d’avoir trouvé des pièces disponibles commercialement pour assembler ce premier prototype. Puisque le dispositif est à faible régime cyclique, l’utilisation d’un supercondensateur d’une couple de farads pourrait être envisagé, afin de réduire la masse de la batterie. Un heatsink plus compact et l’utilisation d’un micro-ventilateur pourrait aussi aider à réduire le volume. Mais la réalité est que pour avoir un effet convainquant, on doit se situer dans le régime thermique de l’ordre de 1W. Cela demande un peltier d’une taille conséquente (environ 1cm^2) ainsi qu’une alimentation pouvant fournir suffisamment de courant (~1A). De plus, l’utilisation du blutooth pour être complètement sans fil est un requis dans une applcation portative, ce qui demande un certain espace supplémentaire.

Vient ensuite le cas d’utilisation. Offrirait-il un avantage dans le cas d’une manette de jeu vidéo par rapport aux dispositifs haptiques déjà existants? L’intégration de « pixels » de froid est-elle une avenue? (Google donne quelques projets de recherche intéressants, qui sont tous plutôt loin de la commercialisation).

Pour ce qui est du transfert d’émotions par le toucher, puisque la constante de temps est assez longue, ce serait limité à des événements très ponctuels. On pourrait penser le corréler avec une métrique plus aboutie que le rythme cardiaque ou la respiration. Par exemple, le niveau d’arousal ou de plaisir, etc. Les limites à cet effet sont plutôt du côté des capteurs et des algorithmes de mesure. Ce qui est intéressant avec une haptique basée sur la température, c’est qu’il serait plus facile d’intégrer un stimuli proche de la limite du conscient, c’est-à-dire qu’on s’en rendrait compte seulement en y portant attention. Cela pourrait offrir un intermédiaire intéressant dans le cas où un système numérique comme un ordi ou un cellulaire cherche à attirer l’attention de manière non-urgente et douce. À voir si le besoin existe et s’il s’agit d’une piste intéressante.

Premiers résultats du polarimètre

Avec le laser stabilisé en température et le polarimètre version 1, j’ai pu mesurer les états de polarisation suivants :

Mélange de deux polarisations (non polarisé)

Le signal est théoriquement une ligne DC. Ici, une polarisation est légèrement plus importante qu’une autre. Il y a aussi un effet de léger désalignement entre le plan de la lame quart d’onde qui tourne, ce qui fait bouger un tout petit peu le faisceau sur la photodiode.

Explication : la lame quart d’onde a soit aucun impact (zéro degré par rapport à la polarisaion), soit transforme une partie en polarisation circulaire. Puisque les deux polarisations orthogonales ont une intensité égale, la moitié passe le polariseur devant la photodiode et le signal reste constant.

Polarisation horizontale

Dans le cas de la polarisation horizontale, le signal sinusoidal oscille au-dessus d’une valeur DC qui correspond à la moitié de l’intensité.

Explication : La lame quart soit n’a aucun impact (signal maximal, toute la polarisation passe le polariseur devant la photodiode), soit retire la moitié du signal lorsqu’elle est à 45 degrés (la polarisation est complètement transformée en circulaire, et le polariseur filtre la moitié du signal).

Polarisation verticale

Pour la polarisation verticale, le signal sinusoidal oscille entre zéro et la valeur de la moitié de l’intensité

Explication : La lame quart d’onde soit n’a aucun impact, à zéro degré (le polariseur bloque complètement la polarisation à 90 degrés, signal à 0), soit elle convertit complètement le signal en polarisation circulaire lorsqu’elle est à 45 degrés (le polariseur en filtre la moiité et le signal est à 1/2 de la valeur d’intensité).

Transition entre les deux polarisation orthogonales

Polarisation circulaire

Dans le cas de la polarisation circulaire, la fréquence du sinus, pour une même vitesse de moteur et d’échantillonage de la photodiode, la période double et le signal oscille entre zéro et la valeur maximale. La lame quart d’onde est placée à 45 degrés de l’axe de polarisation du laser.

Explication : La polarisastion circulaire est convertie en polarisation linéaire par la lame quart d’onde, qui tourne ensuite l’axe de polarisaion linéaire. Cela prend donc une rotation de 90 degrés pour passer d’un maximum à un minimum, parce qu’elle passe à travers un autre polariseur linéaire. La période est deux fois plus grande parce que la lame quart d’onde doit tourner deux fois plus (90 degrés vs 45 degrés).

Mélange entre polarisation linéaire et circulaire

On voit la multiplication des deux fonctions sinusoidales, une fréquence deux fois plus rapide de faible amplitude, et une fréquence de plus grande amplitude.

Développements futurs

Le moteur utilisé est simplement un moteur DC sans encodeur et sans régulation de vitesse, ce qui crée des variations, et l’orientation absolue n’est pas accessible. Pour différencier entre la polarisation linéaire et la polarisation circulaire, on remarque que l’information en fréquence est importante. En faisant la transformée de Fourier, on pourrait retrouver le poids des composantes circulaires et linéaires.

Si on s’attend à n’avoir que de la polarisation linéaire, le système proposé par siliconsam dans son laser régulé en température fonctionne mieux, car on peut échantillonner simultanément les deux états, à l’aide d’un cube beamsplitter polarisant et de deux photodiodes, le tout sans pièces mécaniques et sans traitement de signal, ce qui rend le tout très simple.

La position angulaire absolue de la lame quart d’onde permettrait de différencier entre les deux polarisation circulaires (gauche et droite) qui sont décalés en phase de 90 degrés. En ce moment, en regardant seulement le signal sinusoidale sans tenir en compte de l’angle, c’est impossible de voir la différence, parce que le signal passe de zéro au maximum d’intensité dans les deux cas.

L’information en phase et en fréquence est donc aussi importante que l’intensité dans le temps. Le premier débroussaillage est fait, pour la suite, une meilleure optomécanique est nécessaire.

Stabilisation des modes d’un laser He-Ne

En 2016, pour compléter mon projet d’interféromètre de Michelson, j’avais acheté un laser HeNe en kit, sur ebay. Le tube était tout nu, le power supply était fourni, et le tout venait avec des pièces d’électronique pour stabiliser les modes de polarisation du laser. Puisque la polarisation avait peu d’impact dans mon interféromètre, j’ai simplement utilisé le tube tel quel, en obtenant des bons résulats (ma vidéo ici : https://youtu.be/Cea9mbSiblM )

Récemment, dans mon expérience avec mon polarimètre, j’ai observé une oscillation entre deux polarisations linéaires orthogonales. Le tout se comporte comme si le système est une somme de deux ondes avec une polarisation en x et y (selon une orientation spatiale arbitraire autour de l’axe du faisceau) qui oscillent. En lisant un peu, justement sur le site du kit de stabilisation, j’ai compris que lorsque le laser se réchauffe, le gain est plus élevé pour une polarisation qu’une autre, ce qui fait qu’elle domine momentanément l’émission. Puisque le tube contenant le gaz est à l’intérieur d’un autre tube, ce réchauffement prend une éternité et est soumis aux variations des conditions environnementales (convection, etc.). L’idée de sliconsam (pseudo sur ebay) alias repairfaq.org, est de chauffer le tube en se régulant sur les valeurs des deux polarisations. Pour cela, il utilise un polarizing beam splitter cube orienté de manière à respecter l’orientation naturelle des deux modes du laser. Les instructions se trouvent ici : http://repairfaq.org/sam/manuals/stabins5.htm

Le schéma électronique est le suivant :

Source : http://www.repairfaq.org/sam/uSLC1/uSLC1.htm

Une fois assemblé, ça ressemble à ceci :

J’ai rajouté un ampli transimpédance pour diminuer le bruit. Mon montage optique ressemble à un gros blob de colle, et fixé avec un tie-wrap sur le miroir du faisceau secondaire (ne le dites pas à personne, c’est un peu gênant).

Optique et électronique pour la stabilisation du laser. En vert, le miroir du laser, le cube dépasse un peu du motton de colle chaude. Les deux photodiodes sont à même la surface. Le tout tient à un angle d’environ 15degrés par rapport à l’horizontale, défini par la partie du laser qui a l’entrée de remplissage du gaz.

La pièce imprimée en 3D est celle-ci, mais elle marchait moyen, c’était juste pour me donner une base :

$fn=50;
//rotate([0,90,0]){
difference(){
cube([13,15,15],center=true);
#translate([-6.5,-7.5,0]) cube([13,15,10]);
translate([7,0,0]) rotate([0,-90,0]) cylinder(d=6.5,h=5); //miroir du laser
cube([4.5,4.5,4.5],center=true); //polarizing beam splitter
translate([0,-4.5,0]) cube([7,6,4.5],center=true); //photodiode
translate([-4.5,0,0]) cube([5,7,4.5],center=true);
}

Dans la version actuelle de ce kit, siliconsam fournit un pcb qui contient déjà cet assemblage un peu délicat. Mais toujours pas de manière de le fixer au tube. Bref mon montage n’est pas idéal, mais il fonctionne bien. Le résultat final ressemble à ceci (en incluant ma monture pour ajuster les angles et la hauteur déjà assemblée de mon projet d’interféromètre) :

En utilisant le firmware déjà présent sur l’arduino, qui contient entre autres un PWM (je n’ai pas lu tous les détails), j’ai réussi à observer l’évolution du réchauffement. On voit clairement une accélération de l’oscillation lorsque l’on part le heater, puis un ralentissement lorsque l’algo de stabilisation commence à embarquer.

Source : http://repairfaq.org/sam/manuals/stabins5.htm

Contrairement aux promesses, je n’ai pas réussi à obtenir un lock parfait, cela durait quelques minutes, puis le laser sortait de sa zone de stabilité et recommençait à osciller. Peut-être qu’il y a un bug, je vais essayer d’updater le firmware. Il y a aussi le fait que je l’ai utilisé en hiver avec une température externe démesurément basse de notre mois de janvier, dans une pièce propice aux courants d’air. En outre, le tube n’est pas isolé plus qu’il faut. Prochaine étape, faire un boitier pour tout ça qui isole mieux des conditions environnementales tout en permettant la flexibilité du montage. Peut-être que ça aiderait aussi le délai avant l’obtention d’un lock, qui était d’environ 45min-1h. Disons qu’il ne faut pas être pressé avant de faire des mesures! En tout cas, c’est très intéressant d’avoir un laser contrôlé en polarisation, pouvant fournir l’une ou l’autre sur demande, avec une stabilité en intensité qui dure plusieurs minutes.

Assemblage d’un polarimètre à lame quart d’onde rotative

Le mois passé, je suis tombé sur une boîte contenant du matériel optique que j’avais acheté au bac, inspiré par les laboratoires de Travaux pratiques d’optique photonique et de Fibre optique. Il y avait un paquet de lames quart d’onde en mica (spruce pine mica co part no 1000-0236), une paire de polariseurs circulaires provenant de lunettes de cinéma 3D, et une feuille de polariseur linéaire dichroique. Partant de ce matériel et d’un schéma rudimentaire du polarimètre de thorlabs :

Source : thorlabs.com/newgrouppage9.cfm?objectgroup_id=1564

je me suis dit que j’avais assez d’information en main pour construire mon propre polarimètre, pour la modique somme de pas mal moins que le 5861$US de la version de Thorlabs (ou encore 6750$US pour celui d’Edmund).

La complexité de l’optomécanique réside dans le fait que l’on veut faire tourner la lame quart d’onde selon son axe central, qui est aussi l’axe optique; autrement dit, l’axe de rotation doit être dégagé de tout matériel. C’est pas un requis très commun dans le monde de la mécanique et de la robotique. Après avoir investigué rapidement comment faire tourner un bearing sur lui-même sans grand succès, je suis tombé sur ce kit : Servocity Gear Drive Pan Kit for 37mm Spur Gear Motor. Il est vendu comme la base d’un systèm pan/tilt vertical (axe de rotation autour du z). La particularité est que l’axe central est un tube avec des bearings extérieurs, ce qui est parfait pour mon assemblage.

J’ai imprimé une première pièce pour coller ma lame quart d’onde avec de la colle blanche ordinaire :

$fn=50;
difference(){
cylinder(d=31.5,h=2);
cylinder(d=13,h=2);
}

La lame quart d’onde située à l’intérieur de la pièce rotative, fixée à l’aide de la monture imprimée en 3D

J’ai également placé une photodiode et un polariseur linéaire dichroique pour compléter l’assemblage.

Le polariseur est collé sur la monture du mileu, lui aussi avec de la colle blanche. La photodiode est collée sur la plaque arrière avec de la colle chaude.

Monture pour le polariseur :

$fn=50;

difference(){
cube([42,42,7]);
translate([8,8,0]) cube([26,26,7]);
translate([5,0,7/2]) rotate([-90,0,0]) cylinder(d=4.5,h=42);
translate([42-5,0,7/2]) rotate([-90,0,0]) cylinder(d=4.5,h=42);
}

Monture pour la photodiode :

$fn=50;

difference(){
cube([42,42,7]);
translate([18,0,3]) cube([6,24,4]);
translate([5,0,7/2]) rotate([-90,0,0]) cylinder(d=4.5,h=42);
translate([42-5,0,7/2]) rotate([-90,0,0]) cylinder(d=4.5,h=42);
}

La photodiode est branchée dans un ampli transimpédance au gain ajusté correctement avec une résistance. La sortie est envoyée à un analog input de l’arduino. Le sketch utilisé est simplement l’exemple Graph :

void setup() {
// initialize the serial communication:
Serial.begin(9600);
}

void loop() {
// send the value of analog input 0:
Serial.println(analogRead(A0));
// wait a bit for the analog-to-digital converter to stabilize after the last
// reading:
delay(2);
}

Avec le delay pour ajuster la fréquence d’échantillonage (ici, 500Hz). J’utilise la fonction traceur série intégré à l’IDE (outils -> traceur série)

Un moteur DC de 37mm de diamètre, 1100 rpm et 12V est utilisé avec une simple source de puissance à plusieurs voltages, ce qui permet de sélectionner sa vitesse. L’engrenage du système pan a un ratio 4.2:1 et j’opère le moteur à basse vitesse, environ 30rpm. Dû au couplage inconstant entre les engrenages et la friction, une petite variation dans la vitesse de rotation est observée, ce qui affecte la précision de la lecture de polarisation. Dans une version ultérieure, on pourrait utiliser un stepper qui fournirait une bien meilleure précision angulaire.

C’est un premier prototype pour identifier les défis à relever pour la suite. Dans un prochain article, je parlerai des mesures obtenues.

Comment brancher une photodiode 101

Je mets ça ici parce que je l’oublie périodiquement et que c’est la base de tous mes projets impliquant un mélange d’optique-photonique et d’électronique.

p. 842 du Art of Electronics 3rd edition

Figure A : la photodiode est en mode photovoltaîque (photocourant, en tout cas). Le voltage à sa jonction est de 0V. Le courant est transformé en voltage avec un gain V=RI par l’ampli transimpédance. Cette configuration minimise le dark current, mais est un peu plus lente (la capacitance de la jonction fait un filtre passe-bas avec la résistance). Meilleur SNR, moins de vitesse de réponse. Mode privilégié pour la plupart des projets.

Figure B : la photodiode est biaisée avec un voltage Vb, ce qui diminue sa capacitance, mais ajoute aussi le courant de fuite au dark noise. Le circuit est plus rapide, mais a un SNR plus faible.

Interface logique du picoampèremètre

En suivant scrupuluseument le datasheet du ddc112, j’ai réussi à coder mon TinyFPGA pour faire l’interfaçage logique et envoyer les données à l’ordinateur via UART.

Pour le uart en verilog, j’utilise cet excellent module : https://www.nandland.com/vhdl/modules/module-uart-serial-port-rs232.html

Après queqlues tests infructeux (dont une histoire classique d’ordre des bits : le ddc112 écrit le MSB en premier sur sa pin de donnée, donc il faut remplir le buffer à reculons, en commençant par le MSB également), j’ai réussi à envoyer la charge de test de 13pC (que je lis curieusement à 14pC, mais bon…)

Il y a deux choses qui restent à regarder pour optimiser la lecture : le zéro n’est pas à zéro, mais tourne autour de -0,1pC (1s d’intégration). Il faudra donc faire une calibration pour compenser cet offset. C’est peut-être dû au design de mon PCB, qui laisse fuire ce courant, ou à une erreur Vos dans l’électronique du ddc112. Quoiqu’il en soit, elle est stable, donc une fois compensée, ça devrait aller. Ça reste tout de même de la haute voltige de mesurer des courants aussi minuscules, étant donné que c’est la première fois que je conçois un circuit aussi précis, je reste satisfait de la performance.

Un autre problème consiste en l’alternance binaire entre deux mesures consécutives, qui rajoute une erreur répétable. Voilà ce que ça donne lorsque rien n’est branché en entrée :

input2 :-0.1136pC
input2 :-0.0942pC
input2 :-0.1086pC
input2 :-0.0989pC
input2 :-0.1096pC

Il y a une légère oscillation de 0,02pC, on voit que la mesure peut être plus précise que cette erreur, avec une résolution en-dessous de 0,001pC (1fC !!). Ma compréhension du phénomène, c’est que cette erreur-ci est due à une différence de gain (ou de capacité de l’intégrateur, etc.) entre l’intégrateur A et B. Rappelons brièvement le principe de fonctionnement du DDC112 : afin de pouvoir offrir une mesure qui intègre en continu (sans dead time), le circuit électronique est doublé pour un même channel : pendant que le premier intégrateur intègre la charge, le deuxième est lu et numérisé, une switch alterne entre les deux à chaque nouvelle mesure. Ainsi, la première mesure vient du circuit A, et la deuxième du B, et cela alterne à l’infini. C’est pourquoi une différence de gain entre les deux se traduit par un pattern répétable à toutes les deux mesures.

Heureusement, on peut faire la calibration avec la charge de test de 13pC, et ce, pour tous les ranges utilisés. En ce moment, j’utilise le range maximal, soit 350pC, parce que l’effet est environ 10 fois pire à 50pC (normal me direz-vous!) et que je suis encore bien loin du 20bit théoriquement atteignable pour que cela vaille la peine. Donc cet effet peut aussi être compensé.

Finalement utiliser le port USB du FPGA serait gagnant, pour ne pas avoir recompiler le code verilog à chaque fois que l’on veut changer le range ou la durée d’intégration, et aussi pour faire la calibration. Le Uart reste très utile pour déboguer un code verilog récalcitrant, je garde ça en tête. Mais la communication USB rajoute une couche de complexité. Mon objectif jusqu’à présent était surtout de valider les performances de mon circuit électronique.

J’ai fait des tests avec une photodiode et si mon picoampèremètre dit vrai, je suis capable de mesurer des plages de courant entre 350pC et 0.1pC. Ça teste mes capacités à faire un noir absolu, puisque le signal dépend de la lumière ambiante malgré la boîte métallique et le tape noir, ce qui est génial et indique une sensibilité lumineuse extrême, de l’ordre du nW/cm^2. Ne reste plus qu’à trouver quelque chose d’une aussi faible luminosité à mesurer!

Le code Verilog sur le FPGA:

module picoampmeter (
  input CLK, //16Mhz Clock
  input DDC_DOUT, //Serial output of DDC
  input DDC_NDVA, //Valid data, active low

  output DDC_TEST, //test DDC with charge injection
  output DDC_CONV, //wich side of integrator is converting
  output DDC_CLK, //clk of DDC112 (10MHz nominal)
  output DDC_DCLK, //data clock for transmission
  output DDC_NDX, //enable serial, active low
  output DDC_RA0, //range lsb
  output DDC_RA1, //range
  output DDC_RA2, //range msb

  output USBPU,  // USB pull-up resistor
  output PIN_1 // Serial TX
);
// drive USB pull-up resistor to '0' to disable USB
assign USBPU = 0;

//Uart module
reg enableUartTx; //Enable uart transmitter
reg [7:0] UartByte; //Byte to send trough uart
wire UartDone;
parameter c_CLKS_PER_BIT = 1667; //Baudrate 9600, 16'000'000/9600 = 1667
uart_tx #(.CLKS_PER_BIT(c_CLKS_PER_BIT)) UART_TX_INST
(.i_Clock(CLK),
 .i_Tx_DV(enableUartTx),
 .i_Tx_Byte(UartByte),
 .o_Tx_Active(),
 .o_Tx_Serial(PIN_1),
 .o_Tx_Done(UartDone)
 );

//Select range of integrator
// 001 => 0 to 50pC
assign DDC_RA0 = 1;
assign DDC_RA1 = 1;
assign DDC_RA2 = 1;

//Generate 8MHz clock for DDC CLK
reg clk_8;
always @(posedge CLK) begin
  clk_8 <= !clk_8; //Divide clk by 2
end
assign DDC_CLK = clk_8;

parameter integration_time_us = 15999999;
//Generate integration signal (conv)
reg conv;
reg [23:0] counter = integration_time_us; //1s integration
always @ (posedge CLK) begin
  if (counter == 0)
    begin
      conv <= !conv;
      counter <= integration_time_us;
    end
  else
    counter <= counter - 1;
end
assign DDC_CONV = conv;

//Generate test signal
assign DDC_TEST = 1'b0; //continuous test (13 pC per integration)

//Read serial data
parameter s_IDLE                 = 3'b000;
parameter s_READDDC              = 3'b001;
parameter s_CHECK_READDC_IS_DONE = 3'b010;
parameter s_TRANSFERUART         = 3'b011;
parameter s_WAITUART             = 3'b100;
parameter s_DONE                 = 3'b101;

reg [2:0] StateMachine = 0;
reg dclk;
reg [39:0] buffer;
reg [5:0] bufferpt;
reg r_ndx;
always @ (posedge CLK) begin
  case(StateMachine)
    s_IDLE :
      begin
        r_ndx <= 1'b1; //disable ndx
        dclk <= 1'b0; //disable dclk
        bufferpt <= 39; //start with msb (big-endian)
        if (DDC_NDVA == 1'b0) //Data valid, read serial data output
          begin
            r_ndx <= 1'b0; //Take ndx low to acknoledge data valid
            StateMachine <= s_READDDC;
          end
        else
          StateMachine <= s_IDLE;
      end

    s_READDDC :
      begin

        if (dclk == 1'b1)
          begin
            buffer[bufferpt] <= DDC_DOUT;
            bufferpt <= bufferpt - 1;
          end
        dclk <= !dclk; //toggle clock

        StateMachine <= s_CHECK_READDC_IS_DONE;
      end

    s_CHECK_READDC_IS_DONE :
      begin

        if (bufferpt < 40)
          StateMachine <= s_READDDC;
        else
          begin
            bufferpt <= 0;
            UartByte <= 10; //newline
            enableUartTx <= 1'b1;
            StateMachine <= s_WAITUART;
          end
        // Display Allo! on UART (debug)
        /*bufferpt <= 0;
        buffer[7:0] <= 65;
        buffer[15:8] <= 108;
        buffer[23:16] <= 108;
        buffer[31:24] <= 111;
        buffer[39:32] <= 33;

        // Display 13pC on both channel
        bufferpt <= 0;
        buffer[39:0] <= 40'b0100001010001111010101000010100011110101;
        UartByte <= 10; //newline
        enableUartTx <= 1'b1;
        StateMachine <= s_WAITUART;
        */
      end

    s_WAITUART :
      begin
        if (UartDone == 1'b1)
          StateMachine <= s_TRANSFERUART;
        else
          begin
            enableUartTx <= 1'b0;
            StateMachine <= s_WAITUART;
          end
      end

    s_TRANSFERUART :
      begin
        if (bufferpt < 39)
          begin
            UartByte <= {buffer[bufferpt+7],
                          buffer[bufferpt+6],
                          buffer[bufferpt+5],
                          buffer[bufferpt+4],
                          buffer[bufferpt+3],
                          buffer[bufferpt+2],
                          buffer[bufferpt+1],
                          buffer[bufferpt]};
            bufferpt <= bufferpt + 8;
            enableUartTx <= 1'b1;
            StateMachine <= s_WAITUART;
          end
        else
          begin
            StateMachine <= s_IDLE;
          end
      end

    s_DONE :
      begin
        StateMachine <= s_DONE;
      end

  endcase
  end
  assign DDC_DCLK = dclk;
  assign DDC_NDX = r_ndx;
endmodule // picoampmeter

Le code python pour lire le port série et retransformer les bits en deux channels en nombres float

import serial
ser = serial.Serial('/dev/ttyUSB0', 9600)
fullscale = 350 #50pC pour le scale actuel codé dans le fpga

while True:
    dataByte = ser.readline()
    #print(dataByte)
    if len(dataByte) > 4:
        #print(dataByte)
        data = [int(d) for d in dataByte]
        input1 = float(data[4]*4096 + data[3]*16 + int(data[2]/16) - 4096) / (2**20-1-4096) * fullscale
        input2 = float((data[2]%16)*65536 + data[1]*256 + data[0] - 4096) / (2**20-1-4096) * fullscale
        print("input 1 :{:.4f}pC, input2 :{:.4f}pC".format(input1,input2))

Fourrure kombucha-asclépiade

En lisant l’article de Christine Knobel dans Make: 72, «Growing Leather : Use kombucha to make this versatile vegan textile in your kitchen» (https://youtu.be/Di6HBBw1Esg), il m’est venu l’idée d’essayer la recette et de me mettre — enfin, direz-vous — à la culture du kombucha.

Je méditais vaguement sur le sujet quand j’ai eu une idée folle : et si on y rajoutait de la soie d’asclépiade, juste pour voir? Pour créer un faux cuir ultra-léger avec des poils, quoi de mieux que d’essayer de mélanger kombucha et asclépiade?

L’ampoule dans ma tête s’est allumée lorsque j’ai lu que le SCOBY (la pellicule champignonesque qui se forme à la surface du kombucha lors de la fermentation) était composé à 100% de cellulose, produite par les bactéries lors d’un procédé biochimique extrêmement complexe. Sachant que la soie de l’asclépiade est composée à environ 90% de cellulose, je me suis tout de suite demandé s’il y aurait une compatibilité chimique, si la cellulose du kombucha pourrait «pousser» sur celle de l’asclépiade et ainsi créer une fusion plus parfaite que n’importe quel adhésif ne le pourrait jamais.

La réponse courte, après deux semaines de fermentation et deux semaines de séchage, est oui! Ça fonctionne surprenament bien. J’ai essayé de placer la soie au-dessus, en-desssous et avant que la pellicule ne se forme. La meilleure méthode est de placer la soie avant que la pellicule ne se forme. La soie est hydrofuge et flotte beaucoup, donc peu de surface est véritablement en contact avec le liquide qui génère la pellicule. En la plaçant en-dessous, le poids fait en sorte qu’elle coule un peu plus, mais cela sépare aussi la première pellicule de la deuxième, ce qui fragilise la couche. Si on veut enfermer la soie entre deux couches de cuir de kombucha, vaut mieux le faire en deux étapes : une première fois avec l’asclépiade, et une seconde fois sans, ensuite la fusion s’opérera lorsque l’on réunira les deux feuilles encore gorgées d’eau. En plaçant la soie sur la pellicule déjà créée, elle colle, mais pas au maximum : la pellicule grossit surtout à l’interface entre le solide et le liquide, mais tout de même, cela fonctionne un peu, parce que les bactéries se trouvent un peu partout et cela a pour effet de rendre la pellicule auto-réparatrice tout le temps qu’elle est «vivante», c’est-à-dire gorgée de thé sucré. Le meilleur effet a lieu lorsque la pellicule se crée entièrement autour des soies d’asclépiade en flottaison à la surface du kombucha.

La prochaine expérience sera de l’essayer sur une grande surface, comme décrit dans l’article de Christine Knobel, afin d’avoir une fourrure d’une dimension acceptable pour créer toutes sortes de vêtements. Finalement, l’alignement des soies reste un enjeu, en les laissant pêle-mêle, elles se figent dans le kombucha telles quelles, ce qui est fonctionnel (et potentiellement très chaud), mais moins esthétique que si je trouvais une façon de toutes les aligner dans le même sens. Ce sera un défi en soi — ou en soie! — puisque les fibres sont extrêmement légères, hydrofuges et cassantes, bref, les mêmes problèmes que d’habitude. Les fibres alignées, le résultat donnerait une fourrure extrêmement jolie, douce et chaude.

Conception d’un picoampèremètre à l’aide du ddc112

Je suis tombé, en faisant des recherches sur la section des applications médicales de TI, sur une série de puces faisant la conversion utlra-précise de charges ridiculement petites : 10^-12 Coulombs (http://www.ti.com/product/DDC112) Elles sont utilisés comme convertisseurs analogique-digital dans les CT, entre autres. On peut donc potentiellement faire un détecteur de radiation composé d’un scintillateur, d’une photodiode et de cette puce. J’ai donc décider de me lancer dans la conception d’une carte de test.

 

 

En utilisant KiCAD, j’ai tracé le PCB, en essayant d’appliquer le plus possible les recommandations du datasheet : mettre un plan de ground séparé pour la partie analogique et digitale, et encercler les inputs avec une trace de ground également. J’ai choisi des composants en surface mount assez gros, de manière à être capable de les souder avec un fer standard.

Le PCB une fois assemblé

La soudure a été toute une aventure, mais en y allant méticuleusement, j’ai été capable de tout mettre en place. Il faut simplement y aller très délicatement et mettre le moins possible d’étain, la tension de surface et la qualité du PCB (fabriqué sur OSHPark) font le reste.

J’utiliserai un TinyFPGA pour implémenter le reste de la logique du projet et faire la communication des données vers un ordinateur, j’ai hâte de voir si ma carte fonctionne!

Interfaçage « rapide » entre la caméra D5M et l’écran LCD LT-24 à l’aide d’un FPGA DE0-Nano

Afin de contrôler complètement la caméra, j’ai dû utiliser le port I2C. Après quelques recherches sur les internets, j’ai trouvé un programme VHDL qui fournit la logique d’interfaçage, prenant en entrée un port parallèle et générant un signal I2C de sortie, avec un clock à la bonne fréquence. Merci à Scott Larson pour cet excellent travail, je n’aurais jamais écrit ça moi-même.

Tout ce qu’il me fallait faire (et c’était quand même passablement compliqué), c’était de créer une machine d’état pour écrire dans les registres souhaités de la caméra. Pour cela, je me suis inspiré de la machine d’état pour contrôler l’écran LCD, ainsi que de l’exemple fournit sur le site donné ci-haut.

———————————
— Data feeder
———————————
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity datafeeder is
    port ( feed_clk : in std_logic;
        i2c_busy : in std_logic;
        i2c_data_rd : in std_logic_vector(7 downto 0);
        feed_rst_n : in std_logic;
        ——————————–
        i2c_reset_n : out std_logic;
        i2c_ena : out std_logic;
        i2c_addr : out std_logic_vector(6 downto 0);
        i2c_rw : out std_logic;
        i2c_data_wr : out std_logic_vector(7 downto 0);
        data : out std_logic_vector (15 downto 0));
end datafeeder;
— architecture
architecture my_datafeeder of datafeeder is
    signal busy_prev : std_logic;
    constant slave_addr : std_logic_vector(6 downto 0) := « 1011101 »; — The address of d5m camera is BA write and BB read, the first 7 bits are 1011101 or 5D
    type state_type is (start,getcmd,delay,writereg,readreg,donewr,done); — All the states of the FSM
    signal state : state_type;
    subtype Cmd_t is std_logic_vector (23 downto 0);
    type Commands_t is array (natural range <>) of Cmd_t;
    constant Commands : Commands_t := (     — 8 MSB bits for register address and 16 LSB bits for data
    x »0B » & x »0003″, –Restart, pause acquisition before changing settings
    — Change pixclock to 96MHz (Nope. Max Throughput of pipeline is at 50Mhz for 320×240 picture on LCD
    –x »10″ & x »0051″, –PLL control, (power on : last bit = 1)
    –x »11″ & x »6018″, –PLL config 1, (m factor : 96, n factor – 1 : 24)
    –x »12″ & x »0001″, –PLL config 2, (p factor – 1 : 1)
    –x »10″ & x »0053″, –PLL control, (power on and enable on, last 2 bits)
    — Change image size to 320×240
    x »01″ & x »0042″, –Row start, add 12 to default 54 = 66
    x »02″ & x »0020″, –Column start, add 16 to default 16 = 32
    x »03″ & x »077F », –Row size, 240*8 – 1 = 1919
    x »04″ & x »09FF », –Column size, 320*8 -1 = 2559
    x »22″ & x »0003″, –Row address mode, bin1, skip4, skip 4 pour une image de 640×480 en pattern bayer
    x »23″ & x »0003″, –Column address mode, bin1, skip4, le bin rajoute du flou en déplacement
    x »0B » & x »0001″);–Restart, clear pause_restart bit
    signal Index : integer range 0 to Commands’length; — Index of the command set
    signal Cmd : Cmd_t;
    signal reg_addr : std_logic_vector(7 downto 0);
    signal reg_data : std_logic_vector(15 downto 0);
    signal rw : std_logic;
begin
    state_machine : process(feed_clk,feed_rst_n)
    variable busy_cnt : integer range 0 to 3;
    variable clockcount : integer range 0 to 49999 := 49999; — Delay 1ms between each command
    begin
        if (feed_rst_n = ‘0’) then
            i2c_reset_n <= '0';
            Index <= 0;
            rw <= '0';
            state <= start;
        elsif rising_edge(feed_clk) then
            Cmd <= Commands(Index);
            case state is
                when start =>
                    i2c_reset_n <= '1';
                    i2c_ena <= '0';
                    state <= getcmd;
                when getcmd =>
                    reg_addr <= Cmd(23 downto 16);
                    reg_data <= Cmd(15 downto 0);
                    state <= delay;
                when delay =>
                    if rw = ‘0’ then
                        if clockcount = 0 then
                            state <= writereg;
                        end if;
                        clockcount := clockcount – 1;
                    else
                        state <= readreg;
                    end if;
                when writereg => –state for conducting this transaction
                    busy_prev <= i2c_busy; --capture the value of the previous i2c busy signal
                    if(busy_prev = ‘0’ AND i2c_busy = ‘1’) then –i2c busy just went high
                    busy_cnt := busy_cnt + 1; –counts the times busy has gone from low to high during transaction
                    end if;
                    case busy_cnt is –busy_cnt keeps track of which command we are on
                        when 0 => –no command latched in yet
                            i2c_ena <= '1'; --initiate the transaction
                            i2c_addr <= slave_addr; --set the address of the slave
                            i2c_rw <= '0'; --command 1 is a write
                            i2c_data_wr <= reg_addr; --write register address
                        when 1 =>
                            i2c_rw <= '0'; --command 2 is a write
                            i2c_data_wr <= reg_data(15 downto 8); --write msb bits of data to register
                        when 2 =>
                            i2c_data_wr <= reg_data(7 downto 0); --write lsb bits of data to register
                        when 3 =>
                            i2c_ena <='0'; --disable i2c module, all commands are passed to latch
                            if (i2c_busy = ‘0’) then –i2c module has finihsed
                                busy_cnt := 0; –reset busy counter
                                state <= done;
                            end if;
                        when others => null;
                    end case;
                when readreg =>
                    busy_prev <= i2c_busy; --capture the value of the previous i2c busy signal
                    if(busy_prev = ‘0’ AND i2c_busy = ‘1’) then –i2c busy just went high
                        busy_cnt := busy_cnt + 1; –counts the times busy has gone from low to high during transaction
                    end if;
                    case busy_cnt is
                        when 0 => –no command latched in yet
                            i2c_ena <= '1'; --initiate the transaction
                            i2c_addr <= slave_addr; --set the address of the slave
                            i2c_rw <= '0'; --command 1 is a write
                            i2c_data_wr <= reg_addr; --data to be written
                        when 1 =>
                            i2c_rw <= '1';
                        when 2 =>
                            i2c_rw <= '1';
                            if (i2c_busy = ‘0’) then
                                data(15 downto 8) <= i2c_data_rd;
                            end if;
                        when 3 =>
                            i2c_ena <= '0';
                            if (i2c_busy = ‘0’) then
                                data(7 downto 0) <= i2c_data_rd;
                                busy_cnt := 0;
                                state <= done;
                            end if;
                        when others => null;
                    end case;
                when done =>
                    if Index=Commands’length then — last command has been sent
                        –state <= donewr;
                        state <= done;
                        data <= x"FF00";
                    else
                        Index <= Index + 1; -- fecth next vector
                        state <= getcmd;
                    end if;
                when donewr => — read in loop all registers written
                    rw <= '1';
                    Index <= 0;
                    state <= getcmd;
            end case;
        end if;
    end process state_machine;
end my_datafeeder;

Par la suite, j’ai modifié le processus choosepix, vu que le format de l’image produit par la caméra est déjà le bon. Je prends une image de 640×480 en bayer, vu que je veux une 320×240 rbg, je dois prendre deux fois plus de pixels sur la caméra pour ramener les couleurs ensemble.

chosepix : process(Xpos_in,Ypos_in)
begin
    if ((Xpos_in < 640) and (Ypos_in < 480)) then -- for a 320x240 picture
        Xposchosen <= Xpos_in mod 2; --Read bayer pattern (skip 1 col)
        Yposchosen <= Ypos_in mod 2; --Read bayer pattern (skip 1 row)
    else
        Xposchosen <= (others=>‘1’);
        Yposchosen <= (others=>‘1’);
    end if;
end process chosepix;

J’ai dû changer pixelmix aussi parce qu’il y avait un bug avec le fifo, qui faisait que je lisais la sortie avant le coup d’horloge, donc l’image rouge était décalé horizontalement d’un pixel. Pour le résoudre, j’ai créé un latch et créé un front montant sur la pin rd du fifo avant la lecture de la mémoire :

pixelmix : process(D5M_PIXCLK,Yposchosen,Xposchosen,q_in)
begin
    if (rising_edge(D5M_PIXCLK) and (Yposchosen = 1) and (Xposchosen = 0)) then
        pixImage_out(4 downto 0) <= pixel_in; -- blue value of output pixel
    end if;
    if ((Yposchosen = 1) and (Xposchosen = 1)) then
        if (rising_edge(D5M_PIXCLK)) then
            rdreq_out <= '1';
            pixImage_out(10 downto 5) <= pixel_in & '0'; -- green value of output pixel
        end if;
    else
        pixImage_out(15 downto 11) <= q_in; -- red value of output pixel
        rdreq_out <= '0';
    end if;
end process pixelmix;

Finalement, je ne sais pas pourquoi, mais tout ça a rajouté un petit délai lors de l’écriture des pixels sur l’écran LCD, je l’ai rajout en faisant un not sur la clock newpixclk pour l’inverser, mais ce n’est vraiment pas très beau. En tout cas, ça fonctionne!!