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))

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *