DVDピックアップ2個と、AnalogDiscovery2を使うことで、レーザー走査型顕微鏡を簡単に作ることができました。
必要なもの
- DVDピックアップ ×2個
- HOP-150Xを使った
PHR-803T
も使えると思います
- Analog Discovery 2 ×1個
- たぶん初代Analog Discoveryでも動くと思う
- 抵抗 100Ω ×2個、 470Ω ×2個
- ケーブルとか
- 固定する台座とか
今回はポリウレタン線で配線を引き出しましたが、細かい箇所が多いので苦痛です。
0.5mmピッチ・26ピンのフレキケーブルとフレキコネクタ変換基板を用意すれば、簡単に配線できると思います。
( AliExpressで買えるフレキ変換基板の例 これが適合するかどうかは不明です。ピン数が合わないですが、フレキを縦に切って26ピンにすれば使えるのでは? )
概要
試料に対して、レーザースポットを縦横に走査し、反射光の強度を測定して画像化することで、顕微鏡を作れます。
ピックアップの対物レンズには、フォーカスとトラッキングの2個のボイスコイルモータが付いており、2軸に動かすことができます。
顕微鏡を作るためには、フォーカスのZ軸に加えて、X軸・Y軸のそれぞれを駆動し、走査する必要があります。対物レンズの2軸では1軸足りないので、もう1軸を駆動する機構を作らなければなりません。
足りない1軸を補う方法は、いろいろな手法が提案されています。
- ピックアップをもう一個用意し、対物レンズ上に試料を固定し、ボイスコイルモータで動かす
- GaudiLabs
- 利点: 作るのが簡単。特性も良い。
- 欠点: 大きい試料は観察できない(重さは1g程度が限界だろう)。可動範囲が狭い。
- ステッピングモーターで駆動するリニアステージを使う
- loetlabor-jena
- 利点: 大きい試料も観察できる。可動範囲が大きい。
- 欠点: 作るのが難しい。安物は特性が悪い(バックラッシ、ステップ分解能、など)
- スピーカーでピックアップを動かす
今回は、一番簡単な「ピックアップをリニアステージとして使う方法」を採用します。
リニアステージとして使うピックアップの対物レンズに、両面テープで試料を固定します。
回路図
これを真面目に作ると、出力電流が100mA程度のパワーオペアンプでボイスコイル駆動回路を組んだり、PDIC出力信号をADCで読み取る回路を作ったり、マイコンのソフト書いたりと、かなり面倒です。
なので、 面倒なことは全部AnalogDiscovery2に押し付けよう をテーマに設計しました。
+5V電源は、AD2のSupplyのV+から取ります。
3本のアナログ電圧出力を使って、XYZ軸のすべてをボイスコイルモータで動かします。
ステッピングモーターと違い、ボイスコイルモータは電圧を変えるだけで位置を決められるので、制御が非常に簡単です。
(高速で動かすとなると、慣性や運動特性を考慮しなければならないので難しくなる。位置を観測できないからオープンループ制御でやらなければならない。)
信号出力のch1,ch2をボイスコイルにつないで、対物レンズをフォーカス方向とX軸方向に動かします。
SupplyのV-は、Y軸を担当する2個目のピックアップのボイスコイルモータを駆動します。これだと可動域の半分しか使えないですが、仕方ない。
PDICとLDのピンアサインは、前回記事 HOP-150Xの解析を参考にしてください。

プログラム
AnalogDiscovery2付属ソフト「Waveforms」で使える、プロジェクトファイルです。 dvd_pickup_laser_microscope.dwf3script
フォーカス合わせプログラム
画像を取る前に、コイツを実行してピント合わせをやってください。
出力される数値が±200の範囲内ならば、ピントが合っていると判断してよいと思います。
±200の範囲外のときはフォーカスが合っていないので、対物レンズと物体の距離を調節してください。
本来ならば非点収差法でピント合わせするのが正しいですが、そのためのアナログ回路を作る必要があったのでやめました。
対物レンズの位置を動かして反射光が最大になる位置を探す、という動作になっています。
// This script adjusts the Wavegen offset based on Scope measurement.
clear()
if(!('Wavegen' in this) || !('Scope' in this) || !('Supplies' in this)) throw "Please open a Scope and a Wavegen instrument";
function wavegen_dc(channel,value) {
const channel = channel == 2 ? Wavegen.Channel2 : Wavegen.Channel1;
channel.Mode.text = "Simple";
channel.Simple.Type.text = "DC";
channel.Simple.Offset.value = value;
}
function supply_positive(value) {
Supplies.Output.PositiveSupply.Voltage.value = value;
Supplies.Output.PositiveSupply.Enable.value = 1;
}
function supply_negative(value) {
Supplies.Output.NegativeSupply.Voltage.value = value;
Supplies.Output.NegativeSupply.Enable.value = 1;
}
function init() {
wavegen_dc(1,0);
wavegen_dc(2,0);
supply_positive(5);
supply_negative(2.75);
Wavegen.run();
Supplies.run();
Scope.single();
Scope.Time.Position.value = 5E-3;
Scope.Time.Base.value = 10E-3;
}
function get_scope_avg() {
return Scope.Channel1.measure("Average");
}
function search_focus() {
function func(min,max,div,epsilon) {
const step = (max - min) / div;
var peak_value = -99999;
var peak_voltage = 0;
for(var voltage = min; voltage <= max; voltage += step) {
wavegen_dc(1,voltage);
Scope.single();
if(!Scope.wait()) throw "Stopped";
wait(1)
Scope.single();
if(!Scope.wait()) throw "Stopped";
Scope.single();
if(!Scope.wait()) throw "Stopped";
const value = get_scope_avg();
if(peak_value <= value) {
peak_value = value;
peak_voltage = voltage;
}
}
print(peak_voltage*1000 + " - " + peak_value)
if( step < epsilon ) {
wavegen_dc(1,peak_voltage);
return peak_voltage;
} else {
return func(peak_voltage - step * 1.5,peak_voltage + step * 1.5, div, epsilon);
}
}
return func(-0.22,0.22,10,5E-4);
}
init();
search_focus();
画像取得プログラム
実行する前に、FILENAME
とSCAN_LINES
を変えてください。
SCAN_LINES
は、縦方向の画素数です。大きくすると画像が詳細になりますが測定完了までの時間が長くなります。
まず50 – 200あたりで荒い画像を取得し、良さそうなら400 – 4000くらいで本番撮影するとよいでしょう。
出力されたCSVファイルを画像に変換すると、撮像結果が得られます。
0-255の範囲に正規化して、ガンマ補正とかヒストグラム均等化を行うといい感じの画像になります。
// CHANGE IT !!!
const FILENAME = "E:/temp/acquisition.csv";
const SCAN_LINES = 100;
clear()
if(!('Wavegen' in this) || !('Scope' in this) || !('Supplies' in this)) throw "Please open a Scope and a Wavegen instrument";
function wavegen_dc(channel,value) {
const channel = channel == 2 ? Wavegen.Channel2 : Wavegen.Channel1;
channel.Mode.text = "Simple";
channel.Simple.Type.text = "DC";
channel.Simple.Offset.value = value;
}
function supply_positive(value) {
Supplies.Output.PositiveSupply.Voltage.value = value;
Supplies.Output.PositiveSupply.Enable.value = 1;
}
function supply_negative(value) {
Supplies.Output.NegativeSupply.Voltage.value = value;
Supplies.Output.NegativeSupply.Enable.value = 1;
}
function init() {
wavegen_dc(2,0);
supply_positive(5);
supply_negative(0.5);
Wavegen.run();
Supplies.run();
Scope.run();
}
function get_scope_avg() {
return Scope.Channel1.measure("Average");
}
function scan() {
const x_max = -0.2;
const x_min = 0.2;
const x_step = (x_max - x_min) / 20;
const y_min = 0.5;
const y_max = 5.0;
const y_step = (y_max - y_min) / SCAN_LINES;
Scope.Time.Position.value = 110E-3;
Scope.Time.Base.value = 200E-3;
Wavegen.Channel2.Mode.text = "Simple";
Wavegen.Channel2.Simple.Type.text = "Triangle";
Wavegen.Channel2.Simple.Offset.value = x_min;
Wavegen.Channel2.Simple.Amplitude.value = (x_max - x_min);
Wavegen.Channel2.Simple.Period.value = 800E-3;
const arr = [];
var count = 0;
for(var y = y_min; y <= y_max; y += y_step) {
supply_negative(-y);
wait(0.1);
Scope.single();
if(!Scope.wait()) throw "Stopped";
arr.push(Scope.channel[0].data);
FileAppendLine(FILENAME, Scope.channel[0].data);
count++;
if( count % 10 == 0) {print(count);}
}
}
init();
scan();
print("END");
おわりに
次の改良では、デジタルカメラの手振れ補正に使われるアクチュエータを使うというアイデアを試してみようと思っています。
ピクセルピッチ5μmのAPS-Cサイズのイメージセンサを高速かつ精密に動かす能力のあるアクチュエータなので、きっといい感じに動いてくれると思います。
ボイスコイルモータの運動特性(慣性とか)を考慮していないため、速度が不均一になっていて画像の左半分が歪んでいます。
運動特性を考慮した駆動波形による制御も、いろいろ試してみたいですね。
フィードバック制御ができれば一番いいのですが、位置を検出する方法がないので、オープンループ制御しか無理です。

撮影結果


素晴らしい!! このアイデア、初めて知りました!