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.
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.
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 :
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))
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.
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.
Les deux faces du PCB réalisé à l’aide du logiciel KiCAD
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!
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!!
Pour réussir cet exploit, je suis parti de mes deux réussites précédentes, l’interfaçage de la caméra ainsi que l’interfaçage de l’écran LCD. Ce serait simple de joindre les deux bouts, n’est-ce pas? Eh bien, pas vraiment. La caméra produit une image de 2592×1944 pixels, en patron Bayer (alternance de rouge, vert, bleu). L’écran a 320×240 pixels et son format de couleur est 16bits (5 premiers bits bleus, 6 bits du milieu vert et 5 derniers bits rouges). Il faut donc adapter la taille de l’image, et garder une couleur en mémoire pour toute une rangée, afin de l’appliquer au bon pixel de la ligne suivante.
J’ai donc créé l’entité image_transform afin de faire ces deux étapes, en partant des signaux produits par mon pixel_interface, qui me donne Xpos et Ypos, les signaux donnant la position du pixel sur l’écran.
Un premier processus, choosepix, permet de sauter 7 pixels sur 8 et de produire deux signaux, Xposchosen et Yposchosen correspondant au modulo, ce qui fait que le cycle du compte de 0 à 7 se répète 320×240 fois.
Pour la première ligne du patron de Bayer, j’enregistre seulement le pixel rouge, avec le processus pixelsave. Celui-ci utilise un FIFO créé avec l’utilitaire de Quartus II (megafunction), j’ai essayé de le programmer moi-même, sans grand succès, j’ai donc décidé de remettre cette partie de plaisir à plus tard et de me fier aux ingénieurs d’ALTERA pour cette partie-là. C’est probablement la mémoire la plus simple d’utilisation, elle a très bien fait le travail. Cela m’a permit de découvrir la différence entre les éléments logiques LE (les blocs lego de base pour générer n’importe quel circuit logique) et l’embeded memory (bloc lego de base pour générer n’importe quelle mémoire). J’ai pas trop poussé ma compréhension de cette chose, mais c’est très intéressant. Pour l’instant, c’est simplement bon à savoir, car ça permet d’économiser les registres logiques (mais bon, je suis encore très loin de remplir le FPGA de toute manière).
Dans la deuxième ligne, à l’aide du processus pixelmix, je reconstruis le pixel en 16 bits, en lisant d’abord le bleu, puis le vert, puis la mémoire du rouge correspondant à la première adresse de la valeur précédente. Je vide ainsi toute la mémoire en repassant sur la ligne précédente.
Finalement, l’écran LCD demande une clock à chaque fois que je veux écrire un nouveau pixel dans sa mémoire, je génère la fréquence et la phase appropriée à l’aide du processus pixclkgen, à partir des signaux Xposchosen et Yposchosen. J’ai rajouté l’état suivant à la machine d’état de l’écran LCD, qui apparaît après l’initialisation complète :
when CamLiveFeed =>
LT_DBi <= pixImage_in; -- Write pixel to LCD data port
LT_WR_n <= newpixclk_in; -- toggle write pin with pixel clock
State <= CamLiveFeed; -- deadlock
Voici mon entité image_transform :
———————–
— Image transformation
———————–
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity image_transform is
port( D5M_PIXCLK : in std_logic;
pixel_in : in std_logic_vector(4 downto 0);
Xpos_in : in unsigned(11 downto 0);
Ypos_in : in unsigned(10 downto 0);
q_in : in std_logic_vector(4 downto 0);
———————————–
aclr_out : out std_logic;
data_out : out std_logic_vector(4 downto 0);
rdclk_out : out std_logic;
rdreq_out : out std_logic;
wrclk_out : out std_logic;
wrreq_out : out std_logic;
pixImage_out : out std_logic_vector(15 downto 0);
newpixclk_out : out std_logic);
end image_transform;
— architecture
architecture my_image_transform of image_transform is
signal Xposchosen : unsigned(11 downto 0);
signal Yposchosen : unsigned(10 downto 0);
begin
chosepix : process(Xpos_in,Ypos_in)
begin
if ((Xpos_in < 2560) and (Ypos_in < 1920)) then -- for a 320x240 picture
Xposchosen <= Xpos_in mod 8;
Yposchosen <= Ypos_in mod 8;
else
Xposchosen <= (others=>‘1’);
Yposchosen <= (others=>‘1’);
end if;
end process chosepix;
wrclk_out <= not D5M_PIXCLK; -- write and read fifo at falling edge of the clock
rdclk_out <= not D5M_PIXCLK;
pixelsave : process(D5M_PIXCLK,Yposchosen,Xposchosen) — save first row each 8 rows and skip 8 pixels on x axis to match output resolution
begin
if ((Yposchosen = 0) and (Xposchosen = 1)) then
if (rising_edge(D5M_PIXCLK) ) then
wrreq_out <= '1';
data_out <= pixel_in; -- put the first pixel row in memory buffer
end if;
else
wrreq_out <= '0';
end if;
end process pixelsave;
pixelmix : process(D5M_PIXCLK,Yposchosen,Xposchosen)
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
pixImage_out(15 downto 11) <= q_in; -- red value of output pixel
end if;
else
rdreq_out <= '0';
end if;
if (Yposchosen = 2) then
aclr_out <= '1';
else
aclr_out <= '0';
end if;
end process pixelmix;
pixclckgen : process(D5M_PIXCLK) — generates a clock at the pixel_out rate
begin
if (rising_edge(D5M_PIXCLK) and (Xposchosen < 4) and (Yposchosen = 1)) then
newpixclk_out <= '0';
elsif (rising_edge(D5M_PIXCLK) and (Xposchosen >= 4) and (Yposchosen = 1)) then
newpixclk_out <= '1';
end if;
end process pixclckgen;
end my_image_transform;
L’entité du top permet de tout connecter en série, je ne l’inclurai pas ici.
J’étais un peu perplexe lors de l’interfaçage de cette partie du projet. La documentation fournie par Terasic était très superficielle, se contentant de montrer les exemples, codés avec un processeur NIOS (donc, par définition, complètement obscur). Pour ma compréhension personnelle du fonctionnement des FPGA, je voulais que toute la logique soit explicitée en VHDL. Après une recherche approfondie des internets, je suis tombé sur cette « Application Note » provenant d’ALSE. Ils ont enlevé quelques parties du code, pour une raison ou une autre, mais avec un peu de déduction, c’est possible de le reconstruire. Sans ce document, je n’aurais pas réussi à faire grand chose. Ils ont déchiffré le datasheet de plus de 200 pages de l’ ILI9341 pour sortir les registres dans lequel il faut absolument écrire pour initialiser l’écran.
Le programme d’ALSE utilise entre autres une machine d’états pour envoyer des commandes par le port d’interface parallèle, en tenant compte de toutes les spécificités du contrôleur. Cela m’a permis tout d’abord de comprendre comment programmer une machine d’état en VHDL (mon cours de circuits logique était loin, et on n’a jamais vu ça!)
Un compteur (tickclk) permet de produire une horloge qui produit un pulse à toutes les 1ms, ce qui permet de passer de commande à commande plutôt lentement.
Un array de commandes contenant à la fois le délai nécessaire après l’écriture de celle-ci sur le port, le contenu (adresse ou data) et la valeur en 16 bits à écrire permet de produire toutes les séquences d’écriture nécessaire pour écrire dans tous les registres souhaités. Ensuite, il suffit de lire le datasheet! Le programme ci-dessous est fortement inspiré de l’exemple d’ALSE et permet d’afficher quelque pixels avec la couleur de notre choix.
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity lcdtest is
port ( Clk : in std_logic;
Rst : in std_logic;
——————–
— LT24 graphic Controller
LT_DBi : out std_logic_vector(15 downto 0); — we use it as OUT only
LT_WR_n : out std_logic; — aka WRX
LT_RD_n : out std_logic; — aka RDX
LT_RS : out std_logic; — aka D/CX
LT_CS_n : out std_logic; — aka CSX
LT_RESET_n : out std_logic; — aka RESX
LT_LCD_ON : out std_logic); — Transistor to drive LCD lighting (1 = on)
end lcdtest;
— architecture
architecture my_lcdtest of lcdtest is
subtype Cmd_t is std_logic_vector (28 downto 0);
— Dly_ms(12) & C/D & Data(16) – 29 bits, could be a record
type Commands_t is array (natural range <>) of Cmd_t;
constant Commands : Commands_t := (
— delay_ms D/Cd_n DB — p.83 datasheet ILI9341
x »001″ & ‘0’ & x »0011″, — Exit Sleep
— x »100″ & ‘0’ & x »0001″, — Software reset
x »800″ & ‘0’ & x »0029″, — Display ON
x »001″ & ‘0’ & x »003A », — Set COLMOD register
x »000″ & ‘1’ & x »0055″, — non-default 16 bits RGB data
x »000″ & ‘0’ & x »0036″, — Memory Access Control
— x »000″ & ‘1’ & x »0008″, — non-default BGR filter !
x »000″ & ‘1’ & x »0028″, — non-default BGR filter + row/col inversion for putting the display in 320×240 mode
x »000″ & ‘0’ & x »00F2″, — Enable 3G register
x »000″ & ‘1’ & x »0000″, — non-default = disable 3 gamma — à voir plus tard si je veux une correction gamma ou pas, pour l’instant on va laisser ça comme ça
x »000″ & ‘0’ & x »002A », — Set Column address
x »000″ & ‘1’ & x »0000″, —
x »000″ & ‘1’ & x »0010″, — 16
x »000″ & ‘0’ & x »002B », — Set Page address
x »000″ & ‘1’ & x »0000″, —
x »000″ & ‘1’ & x »000A », — 10
x »000″ & ‘0’ & x »002C », — Memory Write
x »000″ & ‘1’ & x »FFFF », — WHITE
x »000″ & ‘1’ & x »FFFF », — WHITE
x »000″ & ‘1’ & x »FFFF », — WHITE
x »000″ & ‘1’ & x »FFFF », — WHITE
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »F000″, — color
x »000″ & ‘1’ & x »F000″, — color
x »000″ & ‘1’ & x »F000″, — color
x »000″ & ‘1’ & x »F000″, — color
x »000″ & ‘1’ & x »F000″, — color
x »000″ & ‘1’ & x »F000″, — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F », — color
x »000″ & ‘1’ & x »000F »); — color
type state_type is (Boot,Idle,GetCmd,Dly,Wr1,Wr2,Done,Done1); — All the states of the FSM
signal State : state_type;
signal Index : integer range 0 to Commands’length; — Index of the command set
signal Cntr : integer range 0 to to_integer(x »FFF »); — Count of delay between instructions
signal Cycle : integer range 0 to 255; — Counter for the write cycle
signal Cmd : Cmd_t;
constant Nsetup : integer := 5; — Write cycle, try to reduce it to speed up
constant Nhold : integer := 5;
signal Tick1ms : std_logic;
— —————————
— ILI9341 interface
— —————————
— CSX can be kept low
— Write cycle > 66ns = 100 ns (5c = 2 + 3) can work
— Tdst data setup > 10 ns
— Tdht data hold > 10 ns
— /!\ Data read (not used here) is SLOW (500 ns)
begin
LT_RD_n <= '1'; -- we don't read
tickclk : process(Clk)
variable clockcount : integer range 0 to 49999 := 49999;
begin
if falling_edge(Clk) then
if clockcount = 0 then
clockcount := 49999;
Tick1ms <= '1';
else
clockcount := clockcount – 1;
Tick1ms <= '0';
end if;
end if;
end process tickclk;
state_machine : process(Clk,Rst)
begin
if Rst=’0′ then
State <= Boot;
LT_DBi <= (others=>‘0’);
LT_WR_n <= '1';
LT_CS_n <= '1';
LT_RS <= '0';
LT_RESET_n <= '0';
LT_LCD_ON <= '0';
Index <= 0;
Cntr <= 15;
Cycle <= 0;
Cmd <=
(others=>‘0’);
elsif rising_edge(Clk) then
Cmd <= Commands(Index);
case State is
when Boot =>
LT_RESET_n <= '0';
Index <= 0;
if Tick1ms=’1′ then
if Cntr=0 then
LT_RESET_n <= '1';
Cntr <= 300;
State <= Idle;
else
Cntr <= Cntr - 1;
end if;
end if;
when Idle =>
if Cntr=0 then
LT_LCD_ON <= '1'; -- Light ON
LT_CS_n <= '0';
State <= GetCmd;
elsif Tick1ms=’1′ then
Cntr <= Cntr - 1;
end if;
when GetCmd =>
Cntr <= to_integer(unsigned(Cmd(Cmd_t'high downto Cmd'high-11)));
LT_DBi <= Cmd(LT_DBi'range);
LT_RS <= Cmd(LT_DBi'length);
State <= Dly;
when Dly =>
if Cntr = 0 then
Cycle <= Nsetup-1;
LT_WR_n <= '0';
State <= Wr1;
elsif Tick1ms=’1′ then
Cntr <= Cntr-1;
end if;
when Wr1 => — Setup
if Cycle = 0 then
Cycle <= Nhold-1;
LT_WR_n <= '1'; -- rising edge (trig write)
State <= Wr2;
else
Cycle <= Cycle-1;
end if;
when Wr2 => — Hold
if Cycle = 0 then
State <= Done;
else
Cycle <= Cycle-1;
end if;
when Done =>
if Index=Commands’length-1 then — last command has been sent
LT_CS_n <= '1'; -- de-select the interface
State <= Done; -- deadlock (default)
else
Index <= Index + 1; -- fecth next vector
State <= Done1;
end if;
when Done1 => — compensate double pipeline
State <= GetCmd;
end case;
end if;
end process state_machine;
end my_lcdtest;
Ceci est mon premier projet en VHDL, effectué en novembre dernier (désolé, je suis en train de rattraper la documentation des derniers mois 🙂
J’ai d’abord commencé par réussir à amener la clock de 50MHz sur la sortie XCLKIN de la caméra. C’est un simple signal en vhdl :
———————————
— Clock output to the D5M camera
———————————
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
— entity
entity D5M_clock is
port ( CLOCK_50 : in std_logic;
D5M_XCLKIN : out std_logic);
end D5M_clock;
— architecture
architecture myD5M_clock of D5M_clock is
begin
D5M_XCLKIN <= CLOCK_50; -- put 50Mhz clock on the clock input of the camera
end myD5M_clock;
Ensuite, j’ai fait l’interfaçage avec la sortie du pixel sur le port parallèle :
———————
— Pixel Interfacing
———————
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity pixel_interface is
port ( D5M_PIXCLK : in std_logic;
D5M_LVAL : in std_logic;
D5M_FVAL : in std_logic;
–D5M_D0 : in std_logic;
–D5M_D1 : in std_logic;
–D5M_D2 : in std_logic;
–D5M_D3 : in std_logic;
–D5M_D4 : in std_logic;
–D5M_D5 : in std_logic;
D5M_D6 : in std_logic;
D5M_D7 : in std_logic;
D5M_D8 : in std_logic;
D5M_D9 : in std_logic;
D5M_D10 : in std_logic;
D5M_D11 : in std_logic;
———————————————
–pixel : out std_logic_vector(11 downto 0));
pixel_out : out std_logic_vector(5 downto 0);
Xpos_out : out unsigned(11 downto 0);
Ypos_out : out unsigned(10 downto 0));
end pixel_interface;
— architecture
architecture my_pixel_interface of pixel_interface is
signal Xpos : unsigned(11 downto 0);
signal Ypos : unsigned(10 downto 0);
signal newline : std_logic;
begin
pixel_latch: process(D5M_PIXCLK) — latch pixel data
begin
if (falling_edge(D5M_PIXCLK) and (D5M_LVAL = ‘1’) and (D5M_FVAL = ‘1’)) then — read pixel data when it’s valid
pixel_out <= D5M_D11 & D5M_D10 & D5M_D9 & D5M_D8 & D5M_D7 & D5M_D6;
end if;
end process pixel_latch;
position_counter : process(D5M_FVAL,D5M_LVAL,D5M_PIXCLK)
begin
if (D5M_FVAL = ‘0’) then
Ypos <= to_unsigned(-1, Ypos'length); -- -1 to start the first count at 0
elsif (D5M_LVAL = ‘0’) then
Xpos <= to_unsigned(-1, Xpos'length); -- Reset Xpos counter (-1 to start the first count at 0)
newline <= '1';
elsif (falling_edge(D5M_PIXCLK)) then — When a new valid pixel is read
if (newline = ‘1’) then — Increment Ypos count only once a new line
Ypos <= Ypos + 1;
newline <= '0';
end if;
Xpos <= Xpos + 1; -- Increment Xpos count
end if;
end process position_counter;
Xpos_out <= Xpos; -- Final output assignement
Ypos_out <= Ypos;
end my_pixel_interface;
Dans l’architecture, il y a deux process : pixel_latch et position_counter. Pixel_latch est une latch qui permet de lire le port parallèle au bon moment, lorsque la PIXCLOCK est en falling edge (la D5M écrit le pixel sur le port en rising edge) et le met dans un vecteur pixel_out. Position_counter permet de se synchroniser sur les autres sorties de la caméra, soit FVAL (frame valid) et LVAL (line valid) afin de compter les pixels et de savoir la position du pixel actuel en x et en y sur l’image, afin de bien pouvoir reconstruire celle-ci ultérieurement. Le -1 est reconverti en unsigned comme étant la valeur maximale du compteur (2^11-1 pour Xpos et 2^10-1 pour Ypos, ici présent), permettant que la première valeur de position soit 0,0 et non pas 1. Le compteur est incrémenté tant et aussi longtemps que FVAL et LVAL sont à l’état haut. Un signal newline est produit pour compter les lignes (Ypos). La programmation de cette architecture m’a permis de mieux comprendre la différence entre les conditions en programmation classique et celles dans un process de vhdl : il ne peut y avoir qu’une clock que l’on traite, tous les signaux doivent avoir des états définis peu importe ce qu’il se passe, même si on ne s’en sert pas. Bref, la base de la pensée sous-jacente à la conception matérielle des circuits logiques.
Finalement, un dernier composant permet de choisir un seul pixel de l’image et l’afficher sur les DELs de la carte, pour voir si tout fonctionne bien :
—————————-
— Pixel selection to output
—————————-
— library declaration
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
— entity
entity pixel_selection is
port ( pixel_in : in std_logic_vector(5 downto 0);
Xpos_in : in unsigned(11 downto 0);
Ypos_in : in unsigned(10 downto 0);
———————————————
POUT0 : out std_logic;
POUT1 : out std_logic;
POUT2 : out std_logic;
POUT3 : out std_logic;
POUT4 : out std_logic;
POUT5 : out std_logic);
end pixel_selection;
— architecture
architecture my_pixel_selection of pixel_selection is
begin
pixsel : process(Xpos_in,Ypos_in)
begin
if ((Xpos_in = 1296) and (Ypos_in = 972)) then
POUT0 <= pixel_in(0);
POUT1 <= pixel_in(1);
POUT2 <= pixel_in(2);
POUT3 <= pixel_in(3);
POUT4 <= pixel_in(4);
POUT5 <= pixel_in(5);
end if;
end process pixsel;
end my_pixel_selection;
C’était avant que je ne comprenne qu’on pouvait directement mapper les vecteurs dans le pin map, sans avoir besoin d’expliciter chaque bit comme dans le code ci-haut. Mais pour la compréhension de ce qu’il se passe, c’est bien de commencer comme ça : le LSB est relié à la DEL0, etc jusqu’à la DEL5. J’aurais pu utiliser les 8 DELs, mais de toute manière, mon but était de l’interfacer avec un écran en RGB16bit, ce qui a été fait (prochain article! 😉
J’ai réussi, un peu par hasard, à patenter un système pour séparer les soies des graines d’asclépiade plus facilement qu’à la main, en semi-automatisant le processus. J’enlève seulement la coque, puis je place le mélange de soies et de graines dans cette mini-poubelle modifiée pour accueillir un moteur de lave-vaisselle au centre. J’avais imprimé une palette, pensant rendre le tout plus efficace, mais son inertie était trop grande, ce qui maintenait le moteur dans un état statique.
En diminuant le voltage AC à l’aide d’un transformateur variable, on peut introduire des instabilités dans la rotation, ce qui aide à vraiment déloger les graines efficacement, par exemple en inversant le sens de rotation et en produisant des coups aléatoires.
UPDATE! (13 septembre 2020) J’ai trouvé une technique encore plus simple et efficace! Voir la vidéo ci-dessous.
En bref, il suffit de briser l’attachement entre les graines et les soies alors que ces dernières sont encore contenues par la cosse, par des pressions sur le pourtour. De cette manière, elles ne s’échappent pas. Ensuite, il suffit d’ouvrir la cosse et de laisser tomber les graines. Super facile, je ne sais pas pourquoi je n’y avais pas songé auparavant.
J’ai ressorti mon nRF51-DK de la poussière en me disant qu’après deux ans d’absence dans ce projet-là, peut-être que les choses auraient changé concernant le monde mystérieux de l’interfaçage BLE. Je dirais oui et non, car si ça marche un peu plus que dans mes souvenirs, c’est encore plus ou moins sur la coche.
J’utilise bluez 5.50 sur Fedora. Pour une raison obscure, l’interface par défaut de KDE de bluetooth est très aléatoire. J’y vais donc avec le terminal. J’ai commencé par faire un restart : service bluetooth restart, ensuite bluetoothctl pour avoir l’interface de contrôle.
Avec power on et scan on/off, on peut pas mal tout faire.
Donc ce qui est bizarre aussi c’est que mon nRF51-DK doit être nouvellement programmé avec le code. J’utilise celui-ci pour tester l’uart : https://os.mbed.com/teams/Bluetooth-Low-Energy/code/BLE_LoopbackUART/
et j’alterne avec BLE puck juste pour être capable de voir ce qu’il se passe. Donc voici toutes les étapes pour arriver à me connecter avec mon nRF en mode uart. (Avec mbed, on peut compiler en ligne et glisser le fichier hex directement par usb)
power off -> loader le firmware puck ->power on -> scan on -> scan off -> power off -> loader le firmware uart ->power on-> scan on
Ensuite on peut se connecter avec l’interface graphique de bluetooth.
J’ai essayé le code d’Adafruit https://github.com/adafruit/Adafruit_Python_BluefruitLE , mais après beaucoup de gossage (j’ai enlevé le bout du code qui flush la cache pour être capable de voir mon périphérique), il arrive à le trouver et à identifier son UUID comme étant du uart, mais il bogue rendu à device.connect() :
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.UnknownObject: Method « Connect » with signature « » on interface « org.bluez.Device1 » doesn’t exist
Donc ça me semble être un bug dans la librairie d’Adafruit. Avec le connect de fedora, il l’enregistre comme un service uart, je ne sais simplement pas comment y avoir accès par la suite.