Capteur de respiration utilisant une ceinture élastique

Conception

J’ai décidé de redonner un tour de roue à mon projet philati (https://hackaday.io/project/9641-lati), plus précisément au capteur de respiration le composant. J’ai principalement fait un PCB qui contient le pont de Wheatstone, l’ampli d’instrumentation et les filtres analogiques. Un Adafruit Feather nrf52840 Express est utilisé comme microcontrôleur.

La ceinture est faite à partir d’une bande élastique et de « buckles » (attache? j’ai aucune idée du mot en français) que j’ai achetés sur un site de matériel de plein air. La pièce qui tient le PCB et les moteurs haptiques est imprimée en TPU. Voici le code OpenSCAD :

$fn=100;
re = 150; //rayon extérieur
ep = 4; 

difference(){
    cylinder(d=re,h=ep);
    cylinder(d=re-50,h=ep);
    
    //Trous pour la sangle
    translate([re/2-10,-26/2,0]) cube([4,26,ep]);
    translate([re/2-10-8,-26/2,0]) cube([4,26,ep]);
    translate([-re/2+10-4,-26/2,0]) cube([4,26,ep]);
    translate([-re/2+10-4+8,-26/2,0]) cube([4,26,ep]);
    
    //Trous pour les moteurs
    for (theta = [22.5 : 45 : 337.5]){
        rotate([0,0,theta]) translate([(re-25)/2,0,0.3]) cylinder(d=16,h=ep);
    }
}

difference(){
    union(){
        translate([-5,-70]) cube([10,140,ep]);
        translate([-(43+ep)/2, -(56+ep)/2]) cube([43+ep,56+ep,7]);
    }
    translate([-43/2, -56/2,1]) cube([43,56,7]);
}

Le capteur de respiration est toujours le fil en caoutchouc conducteur trouvé sur Adafruit (https://www.adafruit.com/product/519). Il est simplement serré autour de deux broches branchées sur chaque bout du fil. Je ne m’attarderai pas sur la conception du circuit, parce que je l’ai déjà documenté dans mon projet philati. Je mets tout de même ici le schéma et le dessin du PCB, comme référence.

J’ai réussi à à peu près tout rentrer en-dessous du board du feather, pour un design compact et en utilisant uniquement des composantes trough hole.

L’autre partie du projet inclut une évolution de ma ceinture haptique, qui contient 8 moteurs haptiques de vibration en rotation, placées dans l’anneau de TPU, collés sur une mince couche de 0,3 mm d’épais. L’élasticité du matériau permet au moteur de bouger assez librement, tout en étant fixé à la colle chaude. La surface lisse en-dessous de l’anneau peut ainsi être placée directement sur la peau si on veut. J’utilise le PWM sur les pins et un simple buffer logique pour alimenter les moteurs. La spécification en courant ne permet pas d’alimenter tous les moteurs en même temps, mais le maximum que j’ai besoin pour les effets est de seulement deux moteurs à la fois.

Je n’ai pas encore mélangé les deux programmes du microcontrôleur, j’ai mis une pause sur le projet après des tests préliminaires pour les raisons que j’explique plus loin. Le code arduino pour transmettre les données du capteur de respiration à un ordi ressemble à ceci :

/*********************************************************************
 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>
#include "elapsedMillis.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;

#define SAMPLE_LEN 32 //Tel que configuré actuellement, bleuart peut seulement envoyer 64 bytes à chaque 1s
char sendBuffer[2*SAMPLE_LEN]; //Le buffer à envoyer par bluetooth
uint16_t* samplept = (uint16_t*)sendBuffer; //Lit le buffer en le considérant comme du 16 bits
uint8_t samplecnt = 0;
elapsedMicros bufferTimer; // Timer qui surveille le temps entre les enregistrements

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);
  analogReadResolution(14); //Les ADC lisent en 14 bits
  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()
{

  if (bufferTimer >= 100) { //parce que le code BLE est asynchrone, on doit utiliser un timer au lieu du delay
    //À 50ms, on sample à 20Hz
    samplept[samplecnt] = analogRead(A0);
    samplecnt = samplecnt + 1;
    bufferTimer = 0;
  }
  if (samplecnt == SAMPLE_LEN) {
    bleuart.write( sendBuffer, SAMPLE_LEN * 2 );
    samplecnt = 0;
  }
  //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);
}

Le programme qui roule sur l’ordi en python est le suivant :

from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
import array
import numpy as np
import matplotlib.pyplot as plt
import time

ble = BLERadio()

uart_connection = None

plotbuffer = []

#plt.ion()
countdatalen = 0

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]
        starttime = time.time()
        while uart_connection.connected:
            #Pour envoyer des données vers l'appareil
            #s = input("Delay in s : ")
            #uart_service.write(s.encode("utf-8"))
            #uart_service.write(b'\n')
            #Pour recevoir des données
            receiveddata = uart_service.read()
            countdatalen = countdatalen + len(receiveddata)
            print(countdatalen)
            if countdatalen >= 10000:
                print(time.time()-starttime)
                break
            #print(receiveddata)
            #if receiveddata is not None:
                #print(len(receiveddata))
                #plotbuffer.append(array.array('H',receiveddata)) #convertit le bytearray en array de 16bits unsigned
                #plt.plot(array.array('H',receiveddata))
                #plt.show()

Pour tester le feedback de respiration, j’utilise le code arduino suivant :

int mot8 = 12;
int mot7 = 13;
int mot6 = 6;
int mot5 = 5;
int mot4 = 9;
int mot3 = 10;
int mot2 = 11;
int mot1 = 22;

int motarray[8] = {mot1,mot2,mot3,mot4,mot6,mot5,mot7,mot8};

int intensity = 150;

void setup() {
  delay(3000);
}

void loop() {
  //rotations
  for (byte i = 0; i < 8; i = i + 1) {
    //anti-horaire
    //analogWrite(motarray[7 - i],intensity);
    //delay(200);
    //analogWrite(motarray[7 - i],0);
    
    //horaire
    //analogWrite(motarray[i],intensity);
    //delay(200);
    //analogWrite(motarray[i],0);
  }
  
  //random (gouttes de pluie)
  /*
  delay(random(1000));
  int randompin = random(8);
  analogWrite(motarray[randompin],intensity);
  delay(100);
  analogWrite(motarray[randompin],0);
  */
  //vagues (pourrait être cool si rythmé sur la respiration)
  
  analogWrite(mot1, intensity);
  analogWrite(mot8, intensity);
  delay(800);
  analogWrite(mot1, 0);
  analogWrite(mot8, 0);
  analogWrite(mot2, intensity);
  analogWrite(mot7, intensity);
  delay(800);
  analogWrite(mot2, 0);
  analogWrite(mot7, 0);
  analogWrite(mot3, intensity);
  analogWrite(mot5, intensity);
  delay(800);
  analogWrite(mot3, 0);
  analogWrite(mot5, 0);
  analogWrite(mot4, intensity);
  analogWrite(mot6, intensity);
  delay(800);
  analogWrite(mot4, 0);
  analogWrite(mot6, 0);
  analogWrite(mot3, intensity);
  analogWrite(mot5, intensity);
  delay(800);
  analogWrite(mot3, 0);
  analogWrite(mot5, 0);
  analogWrite(mot2, intensity);
  analogWrite(mot7, intensity);
  delay(800);
  analogWrite(mot2, 0);
  analogWrite(mot7, 0);
 
  //pulsation (même chose, synchronisé sur la respiration ça serait cool)
  //(Ca ressemble un peu aux papillons dans le ventre comme effet
  /*
  for (byte value = 50; value < intensity; value++){
    for (byte i = 0; i < 8; i = i + 1) {
      analogWrite(motarray[i],value);
    }
    delay(50);
  }
  */
}

Discussion

Je commence de plus en plus à avoir la question qui trotte en arrière pensée lorsque je travaille sur mes créations : est-ce que ce projet pourraît être un produit? Dans le cas présent, la réponse est non, pour plusieurs raisons. Il y a trop de broches à foin et de colle chaude pour que ça ne demande pas un redesign assez substantiel.

Le capteur de respiration basé sur le caoutchouc conducteur est peu fiable, peu robuste, peu précis, comporte une bonne part d’hystérésis, est difficile à calibrer. Ça marche quand on prend le temps de le faire marcher. Il faut que la ceinture soit fermée avec la bonne tension, pour que l’extension du fil soit maximale et directement relié à la respiration. Cela implique donc que la ceinture soit placée à la bonne place sur les abdominaux à la hauteur du diaphragme, avec une pression assez serée, qui peut rapidement être inconfortable. Le caoutchouc ne peut pas être soudé, il doit être pris en serre contre du métal. Dans philati, j’utilisais des pinces crocodiles. Ici j’utilise un simple noeud autour de la broche métallique. Ça peut tomber facilement et ce n’est pas robuste. Pour une version plus fiable, il faudrait utiliser un crimp avec une ferrule. La meilleure option serait de le coudre directement sur le tissu élastique, afin de rendre le positionnement plus répétable.

Ensuite, la résistance dépend de la longueur, j’ai donc prévu un trimpot afin d’ajuster la plage du pont de Wheatstone à la résistance médiance. Cet ajustement est assez artisanal et doit être fait en portant la ceinture et en regardant la pleine amplitude de la respiration. J’utilise un amplificateur d’instrumentation pour limiter le bruit. Le circuit doit être alimenté à batterie pour éviter de capter le bruit provenant de l’ordinateur par un câble USB.

Le gain est fixé par une résistance, et les fréquences de coupure des filtres analogiques sont aussi fixées par les composantes physiques. C’est donc peu flexible et peu performant, mais c’est facile à faire. Le signal provenant du fil de caoutchouc est assez faible, on parle d’une variation de résistance d’environ 100 Ohm.

En conclusion, le capteur de respiration est difficile à utiliser, doit être potentiellement réajusté à chaque fois qu’on le porte, peu confortable et peu précis.

Pour ce qui est de la partie haptique du projet, plusieurs problèmes ont également été soulevés. Le moteur vient déjà avec un fil, mais est néamoins difficile à souder et peu solide car très petit. J’ai dû souder des rallonges avec des câbles socket female 2,54mm pour brancher dans mon connecteur soudé sur mon PCB. L’assemblage a été assez fastidieux. L’ordre des moteurs a aussi été changé, mais ça s’ajuste facilement dans le firmware.

Le problème principal des moteurs en rotation est le flou artistique dans la vibration produite. Leur accélération est pas toujours pareille, la vibration se couple à l’anneau au complet, même s’il est en TPU et même si le moteur est collé sur une mince bande. Il est difficile de discerner la source exacte, d’autant plus que le couplage mécanique avec la peau et les muscles est plutôt lousse. Pour un meilleur effet, je crois qu’il faudrait carrément coller le moteur à la peau (avec du tape genre) mais ça devient beaucoup trop invasif. De plus, la vibration devient vite désagréable, un peu comme un chatouillement raté, c’est plus gossant qu’autre chose. Le fréquence de la rotation n’est pas la bonne, c’est trop rapide pour être précisément détecté, et trop lent pour être confortable. Le muscle et le cerveau ne savent pas trop comment réagir à ce stimuli.

Ce qui est intéressant comme essai, c’est de pouvoir donner une phase au moteur, et ainsi créer un mouvement. Ici, j’ai essayé de faire un mouvement de haut en bas sur le ventre, partir du diaphragme et descendre les abdominaux, puis remonter, dans le but de donner un cue sur la respiration. C’est possible d’utiliser cela pour synchroniser sa respiration sur ce rythme, mais ça demande tout de même une bonne concentration. Puisque la vibration ne correspond pas du tout au serrement/desserrement des abdominaux, elle crée une sorte de bruit biomécanique, qui désoriente un peu ses perceptions. C’est trop grossier, pas assez confortable. Tout bouge trop. Je ne sais pas non plus quel serait l’effet d’une couche de graisse, mais d’après moi ça ne serait probablement pas glorieux. Finalement, c’est un endroit assez intime le ventre, je ne suis vraiment pas certain que les gens acceptent de porter une ceinture à cet endroit. On n’est pas si loin du bas du ventre, et mon projet n’a pas de prétention érotique.

Conclusion

L’idée derrière ce projet, qui est de mesurer la respiration et d’offrir un feedback haptique pour la synchroniser sur un rythme quelconque, ou entre différents porteurs de l’appareil, est toujours intéressante et mérite d’être explorée davantage. Je vais devoir toutefois repasser sur la planche à dessin et repenser à peu près tout de zéro. Rien dans cette version ne rencontre les critères minimaux pour être intégré dans un prototype viable. On lâche pas! 🙂

Comment ressusciter une batterie Li-po « neuve » qui est à 0V

À cause du taux de décharge de l’ordre de quelques microampères des batteries Lithium-polymère, si celles-ci trainent sur les étagères pendant plusieurs mois — voir plusieurs anées — il arrive que leur voltage tombe à 0V. Elles semblent mortes à tout égard, puisqu’elles refusent de se recharger lorsqu’on les branche dans un chargeur. Pourtant, cela peut arriver à des batteries supposéement neuves (i.e. qui n’ont jamais vécu de cycles de charge et de décharge). Et je me demande même si c’est volontaire, puisque leur envoi par la poste est ainsi beaucoup plus sécuritaire : la batterie ne risque pas de prendre feu si elle est complètement à plat. Peu importe la cause, il y a une façon simple de régler le problème.

Il suffit de brancher la batterie dans un chargeur pour n minutes (durée encore indéterminée, peut-être environ 30 minutes, peut-être moins). La batterie ne se recharge pas et l’indicateur de charge du chargeur ne s’allume pas, mais une certaine magie opère. La batterie se met en quelque sorte dans un état de pré-recharge.

Ensuite, on débranche et on rebranche la batterie pour activer la recharge. Normalement, l’indicateur de charge devrait s’allumer, et la batterie va se recharger à pleine capacité. Si ça ne fonctionne pas, on peut répéter un autre cycle de précharge.

Les raisons profondes de ce mécanisme sont encore à investiguer, mais je pense que cette méthode fonctionne bien, j’ai déjà récupéré trois batteries de la mort de cette façon.

Interface avec une mémoire MRAM

Dans mon exploration des mémoires, je suis tombé sur la technologie MRAM. Elle est à ce jour plutôt dispendieuse (63$ pour la 16Mbit telle que testée (le prix est descendu à 60$, il y a espoir que ça devienne abordable)). Elle a la particularité d’être permanente (l’information reste dessus même lorsqu’elle n’est plus alimentée, comme un mémoire FLASH) et de supporter un nombre illimité de cycles d’écriture (min 10^14). Sa fréquence d’opération peut aller jusqu’à 108MHz. Son interface est SPI et fonctionne à 3,3V, ce qui fait qu’on peut l’utiliser avec n’importe quel microcontrôleur.

Dans mon exemple, je voulais tester un mode d’opération bien particulier. Dans un premier temps, le microcontrôleur initialise la mémoire et envoie le signal pour activer un write d’une durée indéterminée. Ensuite, un circuit externe (ici un micro PDM) envoie des données en continu. La mémoire se comporte comme un buffer circulaire, avec le pointer qui s’incrémente à chaque écriture de bit et revient automatiquement à 0 lorsqu’il atteint la fin de la profondeur. Cela fait en sorte que le circuit est autonome et ne dépend plus des commandes du microcontrôleur, ce dernier pouvant même entrer en sleep, ou gérer à 100% d’autres tâches. La seule autre mémoire que j’ai trouvée qui peut faire ça est la SRAM (article précédent). Cette propriété a le potentiel de simplifier certains designs digitaux, et de limiter la consommation de courant. C’était donc une solution intéressante à bien des égards, outre son coût prohibitif pour bien des applications commerciales.

Voici le code arduino complet :

#include <SPI.h>
int sram_CSn = A2;
int mic_sel = A0;
int mic_clk = 36;

void setup() {
  pinMode(mic_sel, OUTPUT);
  pinMode(sram_CSn, OUTPUT);
  pinMode(mic_clk, INPUT);
  digitalWrite(mic_sel, LOW); 
  digitalWrite(sram_CSn, HIGH);
  Serial.begin(2000000);
  analogWriteResolution(7);
  analogWriteFrequency(200000);

  //Enable write
  SPI.begin();
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
  digitalWrite(sram_CSn, LOW);
  SPI.transfer(0x06); //write enable command
  SPI.endTransaction();
  digitalWrite(sram_CSn, HIGH);
  SPI.end();

  //Test sequential pdm mic
  SPI.begin();
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
  digitalWrite(sram_CSn, LOW);
  SPI.transfer(0x02); //write command
  SPI.transfer(0x00); //address (24 bits)
  SPI.transfer(0x00);
  SPI.transfer(0x00);
  /*
  //Signal triangulaire pour tester
  for (int i = 0; i < 2000000; i++){
    SPI.transfer(byte(i % 256));
    //SPI.transfer(0x00);
    //SPI.transfer(0x00);
  }*/
  for (int i = 0; i < 2097152; i++){ //les adresses vont de 0x000000 à 0x1FFFFF
    //SPI.transfer(0x0F);
    SPI.transfer(0x00);
  }
  SPI.endTransaction();
  SPI.end();
  //digitalWrite(sram_CSn, HIGH);

  //start le micro
  pinMode(mic_clk,OUTPUT);
  analogWrite(mic_clk, 64);
}

void loop() {
  int cmd = Serial.read();
    if (cmd == 114){
      //Ferme le micro et le write infini
      pinMode(mic_clk, INPUT);
      digitalWrite(sram_CSn, HIGH);
      delay(1);

      //Une seonde commande complète de write est nécessaire pour éviter de corrompre les données (fouille-moi pourquoi)
      SPI.begin();
      SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
      digitalWrite(sram_CSn, LOW);
      SPI.transfer(0x02); //write command
      // Magiquement, la mémoire garde son pointeur et écrit à l'adresse où elle était rendue, même si on input 000
      SPI.transfer(0x00); //address (24 bits)
      SPI.transfer(0x00);
      SPI.transfer(0x00);
      for (int i = 0; i < 1000; i++){ //marqueur pour la fin de l'échantillon
        SPI.transfer(0xFF);
      }
      SPI.endTransaction();
      SPI.end();
      digitalWrite(sram_CSn, HIGH);
      delay(1);
      
      //Read
      SPI.begin();
      SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
      digitalWrite(sram_CSn, LOW);
      SPI.transfer(0x03); //read command
      SPI.transfer(0x00); //address (24 bits)
      SPI.transfer(0x00);
      SPI.transfer(0x00);

      uint8_t charbuf[64];
      for (int i = 0; i < 65536; i++){ //lit la mémoire au complet (2Mo (62500*32octets))
        for (byte i = 0; i < 64; i = i + 2){
          charbuf[i] = SPI.transfer(0x00);
          charbuf[i+1] = 0x00;
        }
        Serial.write(charbuf, sizeof(charbuf));
      }

      SPI.endTransaction();
      SPI.end();
      digitalWrite(sram_CSn, HIGH);
      Serial.write(0xCC); //mmh il va falloir que je repense ce mécanisme
      Serial.write(0xCC); //send the stop bytes
    }

}

J’utilise le PWM pour simuler une clock qui alimente le micro PDM. Avec la commande analogWriteFrequency, je choisis une fréquence d’opération de 200kHz. Pour monter aussi haut, il faut jouer avec la résolution à l’aide de la commande analogWriteResolution, que je mets à 7. On active la clock avec la commande analogWrite(pin, 64), puisque la résolution est de 7 bits (0-128).

Pour initialiser la mémoire MRAM, il faut d’abord envoyer la commande 0x06 qui permet d’activer l’écriture dans la mémoire. On a seulement besoin d’envoyer cette commande une seule fois, c’est la protection en écriture. La commande write est 0x02, suivie de l’adresse de 24 bits. Je commence à 0x000000 pour l’adresse.

Pour faire mes tests, j’ai envoyé un signal triangulaire, ainsi qu’écrit la mémoire au complet à zéro pour m’assurer que le test est répétable.

Tant que la pin sram_CSn reste à LOW, le write se poursuit indéfiniment. La mémoire n’a pas besoin de rafraîchissement comme une PSRAM, ce qui fait qu’elle peut se comporter en buffer circulaire sans intervention externe.

Pour éviter que les données soient corrompues, je dois envoyer une seconde commande de write avant de lire la mémoire. Je n’ai pas investigué davantage pourquoi, c’est un bug qui m’a fait perdre beaucoup de temps et j’ai juste été content de le contourner. J’en profite pour écrire une série de 1000 points à la valeur 0xFF, ce qui me permet de distinguer la position de la fin de l’échantillon dans le buffer circulaire plus tard, en assumant que les vraies données ne contiendront jamais un tel signal saturé. C’est utile, puisque je n’ai pas trouvé de manière de savoir où le pointeur d’adresse est rendu lorsque la mémoire est opérée de manière autonome.

La commande de read est 0x03, suivie de l’adresse de 24 bits. Les données sont reçues lors d’un SPI.transmit(), le contenu du write est ignoré, je mets 0x00. Je crée un buffer de 64 octets pour transférer 32 octets de données à la fois dans un serial.print. Cela permet d’optimiser un peu la transmission usb. La fin de la transmission est signalée par le doublon 0xCC 0xCC, qui est détecté dans mon code python. Toutes les autres données sont sur le format 0x00 0xXX, le premier octet est vide et jeté lors de la lecture. Cette méthode est simple et me permet de transmettre 32 octets distincts par paquet, au prix d’une efficacité de 50%.

À 200kHz et même à 1MHz, la consommation en écriture est d’environ 9mA. Ce qui est plus élevé que la mémoire SRAM (1mA) mais plus bas que la mémoire flash (30mA et plus).

Interface avec une mémoire serial SRAM

J’ai testé la puce IS62WVS2568GBLL, qui est une mémoire Serial SRAM de 2Mb. Elle coûte 3,68$ à l’unité. C’est la plus grande capacité que j’ai pu trouver. Son interface est un port SPI. Voici le code que j’ai utilisé pour la tester. J’écris un signal triangulaire de 256 samples, qui se répète sur l’ensemble de la mémoire. J’utilise un fichier .wav pour visualiser les données. La consommation en write continu est d’environ 1mA, à 3.3V d’alimentation et une clock de 200kHz. On peut écrire avec une clock qui peut aller jusqu’à 45MHz (pour une consommation en courant supérieure). On peut écrire en continu sans toggler la pin CS#, ce qui fait que l’écriture peut être contrôlée simplement par la clock, une fois la commande de write envoyée, ce qui en fait un candidat intéressant pour un buffer circulaire indépendant d’un microcontrôleur.

Code arduino :

#include <SPI.h>
int sram_CSn = A2;
int mic_sel = A0;
int mic_clk = A1;

void setup() {
  pinMode(mic_enable, OUTPUT);
  pinMode(sram_CSn, OUTPUT);
  pinMode(mic_clk, INPUT);
  digitalWrite(mic_enable, LOW);
  digitalWrite(sram_CSn, HIGH);
  Serial.begin(115200);

  //Set SRAM to sequential operation
  SPI.beginTransaction(SPISettings(100000, MSBFIRST, SPI_MODE0)); //100kHz
  digitalWrite(sram_CSn, LOW);
  SPI.transfer(0x01); //write mode register
  SPI.transfer(0x40); //01 : sequential mode + bits 0-5 set to 0
  SPI.endTransaction();
  digitalWrite(sram_CSn, HIGH);
  SPI.end();

  //Test sequential write
  SPI.begin();
  SPI.beginTransaction(SPISettings(100000, MSBFIRST, SPI_MODE0)); //100kHz
  digitalWrite(sram_CSn, LOW);
  SPI.transfer(0x02); //write command
  SPI.transfer(0x00); //address (24 bits)
  SPI.transfer(0x00);
  SPI.transfer(0x00);
  for (int i = 0; i < 44100; i++){
    SPI.transfer(byte(i % 256));
    SPI.transfer(0x00);
  }
  SPI.endTransaction();
  digitalWrite(sram_CSn, HIGH);
  SPI.end();

}

void loop() {
  int cmd = Serial.read();
    if (cmd == 114){
      SPI.begin();
      SPI.beginTransaction(SPISettings(100000, MSBFIRST, SPI_MODE0)); //100kHz
      digitalWrite(sram_CSn, LOW);
      SPI.transfer(0x03); //read command
      SPI.transfer(0x00); //address (24 bits)
      SPI.transfer(0x00);
      SPI.transfer(0x00);

      SPI.transfer(0x00); //dummy 8 bytes
      for (int i = 0; i < 44100; i++){
        Serial.write(SPI.transfer(0x00));
      }
      SPI.endTransaction();
      digitalWrite(sram_CSn, HIGH);
      SPI.end();
      Serial.write(0xCC);
      Serial.write(0xCC); //send the stop bytes
    }
}

Code python sur l’ordi :

import serial
import wave, struct

ser = serial.Serial("/dev/ttyACM0", 115200)

ser.write(bytes("r", encoding = 'utf8')) #send read command to get flash memory content

buf = []
while(True):
    char = ser.read(2)
    sample = int.from_bytes(char, byteorder='little') #- 63000 #convert to signed int 16 bits
    if (sample > 32767) or (sample < -32768):
        sample = 0
    if char == b'\xcc\xcc':
        print(len(buf))
        break
    else:
        buf.append(sample)
        #if not (len(buf) % 44100):
        #    print('{}%'.format(int(len(buf) / sizeoffile * 100)), end='\r', flush=True)

#plt.plot(buf)
#plt.show()
with wave.open('sound.wav','w') as obj:
    obj.setnchannels(1) # mono
    obj.setsampwidth(2) #16 bits
    obj.setframerate(22050.0) #sampling frequency
    for value in buf:
        data = struct.pack('<h', value)
        obj.writeframes(data)

Électronisation d’un saxophone

Objectif : partir d’un saxophone fonctionnel et rajouter un genre de MIDI real-time, pour pouvoir en jouer en silence et le brancher sur des synthétiseurs.

Pour la première itération de preuve de concept, j’ai voulu explorer l’utilisation d’un capteur de pression (BMP085, c’est celui que j’avais sous la main) placé sur le trou de la clé d’octave. Je trouvais que c’était la méthode la moins invasive, avant de jouer dans le bec. Voici de quoi avait l’air le montage :

J’ai utilisé la librairie adafruit BMP085 ainsi que le traceur série d’arduino pour aficher les données, à la fréquence maximale.

#include <Adafruit_BMP085.h>

/*************************************************** 
  This is an example for the BMP085 Barometric Pressure & Temp Sensor

  Designed specifically to work with the Adafruit BMP085 Breakout 
  ----> https://www.adafruit.com/products/391

  These pressure and temperature sensors use I2C to communicate, 2 pins
  are required to interface
  Adafruit invests time and resources providing this open source code, 
  please support Adafruit and open-source hardware by purchasing 
  products from Adafruit!

  Written by Limor Fried/Ladyada for Adafruit Industries.  
  BSD license, all text above must be included in any redistribution
 ****************************************************/

// Connect VCC of the BMP085 sensor to 3.3V (NOT 5.0V!)
// Connect GND to Ground
// Connect SCL to i2c clock - on '168/'328 Arduino Uno/Duemilanove/etc thats Analog 5
// Connect SDA to i2c data - on '168/'328 Arduino Uno/Duemilanove/etc thats Analog 4
// EOC is not used, it signifies an end of conversion
// XCLR is a reset pin, also not used here

Adafruit_BMP085 bmp;
  
void setup() {
  Serial.begin(9600);
  if (!bmp.begin()) {
	Serial.println("Could not find a valid BMP085 sensor, check wiring!");
	while (1) {}
  }
}
  
void loop() {
    //Serial.print("Temperature = ");
    //Serial.print(bmp.readTemperature());
    //Serial.println(" *C");
    
    //Serial.print("Pressure = ");
    Serial.println(bmp.readPressure());
    //Serial.println(" Pa");
    
    // Calculate altitude assuming 'standard' barometric
    // pressure of 1013.25 millibar = 101325 Pascal
    //Serial.print("Altitude = ");
    //Serial.print(bmp.readAltitude());
    //Serial.println(" meters");

    //Serial.print("Pressure at sealevel (calculated) = ");
    //Serial.print(bmp.readSealevelPressure());
    //Serial.println(" Pa");

  // you can get a more precise measurement of altitude
  // if you know the current sea level pressure which will
  // vary with weather and such. If it is 1015 millibars
  // that is equal to 101500 Pascals.
    //Serial.print("Real altitude = ");
    //Serial.print(bmp.readAltitude(101500));
    //Serial.println(" meters");
    
    //Serial.println();
    //delay(500);
}

Le bruit est d’environ 2-3 Pa et le signal autour de 5-10 Pa, lorsque je souffle assez fort, mais sans faire vibrer l’anche du bec. On peut donc détecter le souffle de cette manière, mais ça manque de fiabilité. Je pensais que le changement de pression serait plus important, mais il semble que la vitesse du flux d’air est beaucoup plus significative.

À essayer :

  • Les nouveaux capteurs de pression ont un bruit beaucoup plus faible (~0,02 Pa selon les datasheet). À acheter parce que je n’en ai pas.
  • Mettre un micro MEMS pour comparer la sensibilité au signal
  • Essayer avec un micro normal juste pour voir
  • Adapter un design de spiromètre dans le bec

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.

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.

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!

Synchronisation des DEL avec le signal vidéo

Afin de bien pouvoir séparer les signaux de ma caméra oxymétrique, je dois trouver un moyen pour synchroniser les DELs avec l’acquisition des images. L’objectif est d’alterner les prises de vue entre un éclairage avec la DEL rouge et avec la DEL infrarouge, sans que les signaux se chevauchent. Le hic, c’est que la caméra que j’utilise n’a aucun trig externe : toute l’électronique de contrôle est intégrée, laissant seulement le signal vidéo analogique en sortie.

Mon premier essai fut avec une esquisse arduinesque d’une redoutable simplicité :

void loop() {
analogWrite(5,10);
delay(33);
delayMicroseconds(325);
analogWrite(5,0);
analogWrite(6,60);
delay(33);
delayMicroseconds(325);
analogWrite(6,0);
}

La broche enable de chaque régulateur de courant des DELs est reliée aux pin 5 et 6 de l’arduino, qui fait un PWM pour éviter la surchauffe. Essentiellement, le programme allume en alternance les deux DEL, avec une période d’environ 33,333ms, pour correspondre au 30 images par secondes du flux vidéo. Il y a deux problèmes principaux à cette approche : l’horloge de l’arduino n’est synchronisée ni en phase, ni en fréquence avec celle de la caméra, ce qui fait que pendant la durée d’une prise d’image, bien souvent les deux DELs sont allumées séquentiellement, donc on se retrouve avec un signal mélangé. La légère différence en fréquence fait que ce décalage en phase se déplace au fil du temps, produisant un battement lent dans le signal qui rend celui-ci inutilisable périodiquement. Bref, ça marche, mais ce n’est vraiment pas fiable et il y a manière de faire mieux.

J’ai donc relégué l’arduino et me suis décidé à résoudre le problème de manière purement électronique. Le schéma est présenté ci-dessous :

J’ai commencé par observer le signal vidéo analogique à l’oscilloscope. Il ressemble à ceci :

À chaque ligne, il y a un pic vers le bas qui indique une nouvelle ligne. La zone au milieu de la photo survient à la fréquence de 60Hz, puisque la vidéo est interlacée, c’est l’indicateur de nouvelle image. Afin de me synchroniser là-dessus, je me sers de la partie distinctive qui est un plat près de 0V. Je passe donc ce signal dans la borne négative d’un premier comparateur, avec un niveau de voltage assez bas sur la borne positive (224mV), ce qui sort uniquement les pics les plus bas (nouvelle ligne et nouvelle image). La sortie de ce comparateur donne ceci :

Les pics plus importants correspondent au plateau près de zéro que je souhaitais isoler. Je passe ce signal dans un filtre passe-bas, qui va garder seulement la composante à 60Hz et retirer les petites fluctuations :

Je passe ensuite ce signal dans un second comparateur, afin d’obtenir un retour parfait au ground et une forme plus abrupte des pics :

Ça marche bien, sauf que c’est en open collector, donc on ne peut pas l’interfacer directement avec une puce en logique CMOS. Le voltage du niveau haut est à 4,3V environ, ce qui est trop loin du 5V. Après un bon bout de gossage, j’ai réussi à brancher un transistor PNP en configuration d’émetteur commun. Le biasing provoquait des effets bizarres, parce que je mettais une résistance de charge trop élevée, ce qui limitait trop le courant dans le collecteur. En le revirant de bord, (l’émetteur et le collecteur sont inversés), j’ai réussi à le faire marcher par magie. En investiguant un peu, je me suis rendu compte que faire ça diminue le facteur beta, soit le gain en courant du transistor. Autrement dit, la région d’amplification est beaucoup plus faible et il sature très rapidement. En diminuant la résistance de charge, le courant augmente drastiquement et on peut se servir du transistor branché normalement, puisqu’il reproduit bien l’onde carrée. Cela donne un pic inversé qui varie entre 4,9V et 0V :

En envoyant cela dans l’entrée d’horloge d’une bascule T, on crée une onde carrée avec un rapport cyclique de 50%, à la moitié de la fréquence des pulses d’entrée :

Donc après toutes ces étapes, on vient de recréer une horloge qui est synchronisée sur celle de la caméra (avec une phase décalée bien sûr), à 30Hz. Afin de contrôler les DELs, je vais la diviser encore en deux avec une autre bascule T, ce qui va produire deux ondes carrées à 15Hz déphasées de 180 degrés, autrement dit, les deux DELs vont clignoter en alternance à la fréquence de la caméra. Cela donne ceci :

Finalement, je me sers de ce signal pour moduler une autre onde carrée générée par un timer 555 avec un duty cycle faible, qui fait office de PWM pour mes DELs. Le résultat, appliqué sur la broche de contrôle des régulateurs de mes DELs, ressemble à ceci :

Et ça marche tout seul! Lorsqu’on allume le tout, les DELs se mettent à clignoter à la bonne fréquence comme par magie!

Électronique du réfractomètre

Hier j’ai avancé principalement l’électronique de mon projet de réfractomètre. Je suis parti d’un arduino sur breadboard que j’avais déjà construit (en utilisant une puce ATMEGA328 que l’on programme avec un FTDI). J’ai rajouté un écran LCD, avec le programme hello world simplement pour vérifier qu’il fonctionne bien.

Pour l’alimentation du laser, j’utilise un bon vieux régulateur de courant LM350 avec une résistance de 100 ohms. J’ai détruit deux petits modules laser (bon ça vient du dollarama donc c’est pas si grave, mais quand même) : le premier en le laissant branché trop longtemps sur du 5V (il a une résistance pour limiter le courant, mais ce que j’ai découvert c’est qu’elle est faite pour du 4,5V, avec 5V la diode surchauffe et sa puissance de sortie diminue). Je ne m’en suis rendu compte que trop tard, alors que des dommages irréversibles étaient déjà faits. La deuxième diode, je l’ai brisée ce matin, alors qu’un petit peu de colle chaude est tombé directement sur le fil minuscule de l’anode (ou de la cathode, en tout cas), le brisant. Finalement, deux diodes brisées plus tard, tout fonctionne comme un charme.

J’ai modifié l’alimentation du breadboard pour récupérer le VIN du transformateur que l’on branche, et pas seulement le 5V du régulateur, pour pouvoir alimenter le LM350 directement. C’était avec le premier module, où il y avait encore la résistance en série avec la diode laser, ce qui provoquait une chute de voltage. Je ne pense plus que c’est nécessaire à présent, on pourrait utiliser directement le 5V, mais bon, pour l’instant je vais le laisser comme ça puisque ça fonctionne. Ça permet une légère économie d’énergie (mais on s’en fout pour l’instant puisque le système n’est pas à batterie).

Pour ce qui est de la partie de la photodiode, j’utilise un simple amplificateur transimpédance, avec la diode branchée en reverse bias sur le ground, l’entrée positive de l’amplificateur sur le 0V également pour avoir le maximum de sensibilité. Cela est possible car j’utilise un LM358 qui a la possibilité de se rendre jusqu’au ground, autant à l’entrée qu’à la sortie, et ce même en single supply (comme en ce moment dans mon circuit qui l’alimente en 5V). Le désavantage, c’est que son voltage de saturation à l’état haut est aux alentours de 3,7V pour une alimentation de 5V, ce qui n’est pas si grave puisque l’on peut ajuster la pin aref de l’arduino pour correspondre à ce voltage. La résistance de gain est encore à ajuster, mais pour l’instant, je sais qu’elle doit être quelque part entre 100kohms et 1Mohm. Le deuxième amplificateur sur la chip est utilisé comme un voltage follower qui isole l’entrée de l’ADC de l’arduino du circuit.

Le schéma sera publié prochainement!

Je suis en train d’imprimer un couvercle à la partie optique, pour empêcher la lumière ambiante d’interférer dans les mesures (c’est plus simple comme ça, quoi qu’on pourrait éventuellement utiliser un modulateur et un démodulateur à une certaine fréquence pour diminuer le bruit).