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