2020年5月4日月曜日

PICマイコンでBCD変換 その2

以前PICマイコンでBCD変換という記事を書きました。
久しぶりにまたBCD変換を実装しなければならない場面が出てきたので、そういえば昔そんな記事書いていたよなと思って自分のブログを読み返したのですが、そこで参照していた外部サイトがリンク切れを起こしていましたので、訪れた人にとっては理解しにくい記事になってしまっていました。

ですので、そのBCD変換 のアルゴリズムをこの記事にて詳しく説明します。
もしもBCD変換の意義とかそのあたりについてを知りたい人、もしくは具体的な実装を見たい人がいれば上のリンクの記事を見てください。

準備

このようなメモリ空間を用意します。
下の位に2進数、上の位にBCD変換された数が入るようにして、一続きの値になるようにします。以前の記事では、24bit整数型と共用体でビットフィールドを作成し、このようなメモリを実現しました。

さて、実際に計算をこの箱を使いながら試していきます。
今回は例として、「123」をBCD変換してみましょう。123は2進数で表現すると0111 1011です。





n進数

n進数のおさらいです。
普段我々が日常的に使っているのは10進数で、1桁に0~9の10個の数を使います。そしてその桁の数が10を超えたら次の位に1が加算されます。つまり、各桁の値が表しているのは$10^n$ずつの単位になります。
ですので、 $10^n$の位の数字を$d_n$と置くと、各桁の値を使って以下のように表せます。\[10^0d_0+10^1d_1+^2d_2+...\]なんら疑問のない表現ですね。多くの人にとってはくどすぎる解説にすら感じるでしょう。
この考え方は2進数でも同じです。2進数で各桁の値を$b_n$と置くと、2進数の数値は以下のように表せます。\[2^0b_0+2^1b_1+2^2b_2+...\]つまり、上の2進数表現をした123は、すなわち以下のような表現だということです。\[2^7\times0+2^6\times1+2^5\times1+2^4\times1+2^3\times1+2^2\times0+2^1\times1+2^0\times1\\=64+32+16+8+2+1\]さて、話を戻して、この2進数の各桁の数から値を得る式を少し変形してみましょう。\[2^0b_0+2^1b_1+2^2b_2+2^3b_3+2^4b_4+2^5b_5+2^6b_6+2^7b_7\\=b_0+2(b_1+2(b_2+2(b_3+2(b_4+2(b_5+2(b_6+2b_7))))))\]そんなに難しい変形ではないですね。ここで重要なのは、$b_7$を2倍してから$b_6$を足して、それを丸ごと2倍してから$b_5$を足して…という計算で2進数の各桁を数値に変換できるということです。このBCD変換はこの考え方が大きなヒントになります。

左シフト

さて、先ほどの話に戻って、準備で用意したメモリを使って処理をしていきます。
これに左シフト演算を8回行います。ただ単にシフトするだけでは「2進数」エリアにある数字が「BCD10の位」「BCD1の位」の8ビット分に移動するだけですが、ここでは、この「移動するだけ」のシフト演算に意味を見出していきます。
左シフトには「値を2倍にする」という意味合いがあります。10進数で表された数の各桁を一つ左の位に移動させたら値は10倍になりますよね。同じように、2進数でも2倍になります。

BCD1の位を中心に物事を考えると、1回目の左シフトでBCD1の位の最小桁に現れた「0」(下の画像で赤色のもの)は、残り7回のシフトで最終的に$2^7$がかかります。2回目のシフトで現れた「1」(下の画像で緑色のもの)は、残り6回のシフトで最終的に$2^6$がかかります。

このように、左シフトを1回ずつしながら値を動かしていくことで「2進数の各桁の値を倍々にしていって、各桁にふさわしい係数($2^n$)がかかるようにする」という操作をしているとみなすことができます。これは、先ほどの各桁の数から値を導く式とやっていることは同じです。

さて、この操作、メモリ空間上でやっているので2進数でしか物事が見えていませんが、別に入れ物の数の表し方がどうであれ、「2倍して2進数の次の桁を足す」という操作さえ8回繰り返してできれば、示す数値としては元の2進数と同じ意味合いになるはずです。
ですので、左側のBCD3桁分で、BCDに沿ったルールで「2倍して2進数の次の桁を足す」という処理を行っていけば、「BCDで表される元の2進数と同じ数値」が得られるわけです。

BCDとしての「2倍する」

さて、いよいよBCD変換の肝に入っていきます。今度は4bitずつ、「BCDの位」 単位で物事を見ていきます。
前章の最後にも少し触れましたが、BCDの位単位で物事を見ようとも、「2倍してから2進数の次の桁を足す」という計算さえできれば、元の2進数と同じ意味合いの数値が得られます。

ここでBCDで2倍するとはどういうことか、考えてみます。
それは、まぎれもなく左シフトで実現できます。ただし、2進数とは違い、左シフトだけがすべてではありません。BCDの桁は4bitありますので、物理的には0000~1111(2進数)、すなわち0~15(10進数)の16個の数字が入ります。ですがBCDの桁が表すのは10進数の1桁、あくまでも入る数字は0~9です。ですので、左シフトをした後に10以上の数字が入っていたら繰り上げ処理をしてあげれば、問題なく「2倍」の数が得られるはずです。

というわけで実際にやってみましょう。
先ほどの流れで順調に左シフトをしていくと、5回目の左シフトで異変が起こります。
BCD1の位の値が「1111(2進数)」すなわち「15(10進数)」になってしまうのです。ここで桁あふれ処理として、1つ上の位に1を足して、自身は1の位の値だけを保持するという処理をしてあげます。そのシフト繰り上げ処理をしたのが以の図の下2行になります。

「左シフトを8回する過程でこんな変な処理を入れちゃっておかしくならないの?」と思う方もいるかもしれません。
しかし思い出してください。やっていることは「2進数の桁を1桁ずつ加算しては2倍するの処理を繰り返す」ということだけです。その過程で、物事の見方が2進数からBCDに切り替えているのならば、ちゃんとBCDのルールに沿って2倍する処理をしてあげればいいだけです。BCDのルールでは、「各桁の値は10以上になってはいけない」がありますので、超えてしまったらちゃんと次の桁に送ってあげればいい、それだけです。「2進数の桁を1桁ずつ加算しては2倍するの処理を繰り返す」以上のことは何もしていません。ですので、BCDの表現ルールに従ったうえでの結果にはなりますが、数値の意味としては何ら変わりません。

この調子で続けていきます。毎回シフトするたびに各桁が桁あふれしていないか確認し、桁あふれしていれば繰り上げ処理をしてあげます。
 合計8回シフトし終わった時点で、2進数の部分の数値はすべて出払い、BCDの部分には左から順に「1」「2」「3」が格納されました。このようにして、除算を一切使わずにBCD変換ができてしまうのです。

落とし穴

さて、ここまで説明してきた方法には落とし穴というか、考慮されていないパターンがありました。気づいた方はいますでしょうか。これで気づけていたらすごいと思います。

例えば「152(10進数)」=「10011000(2進数)」のBCD変換を考えてみましょう。
4回シフトした時点でBCD1の位が1001(2進数)=9(10進数)となるので、桁あふれはしていません。ですので、このまま進めて左シフトをします。
やっていることは「値を2倍して次の桁を足す」の処理なので、BCD1の位は9を2倍して18、その1はBCD10の位へ繰り上がり、残りの8に2進数のエリアからやってきた次の桁「1」が足し合わされ、9になることが期待されます。 しかし実際に入っていたのは3でした。
このまま続けていくと、最終的にBCDエリアには0001 0000 0100の値が入り、結果は「104」となってしまいます。157とは違う、間違った計算結果になってしまいました。

何が間違っていたかというと、9をそのまま左シフトしてしまったことです。
9を2倍すると桁あふれすることはわかり切っていますが、その後の結果は 「3」となっており、桁あふれを検知できません。すなわち、「BCDの桁あふれ」の章で赤字で書いた「左シフトをした後に10以上の数字が入っていたら」というのは実は間違い(条件不十分)だったわけです。桁あふれを起こしても10以上の数字が入らないことがあるんですね。

先ほども書いたように、9を2倍するということは、1つ上の桁に1が入り、自身の桁には8が入ることを意味します。しかし、単純に左シフトをしてしまうと、1つ上の位には1が入りますが、自身の桁には2が入ります。
それもそのはず、左シフトでは単なる16進数としての扱いなので、9の2倍である12(16進数)をそのまま10進数とみなしてBCD1の位に2が入ってしまっていたのです。

すなわち、従来の「左シフト(=2倍)して10以上ならば繰り上げ処理をする」という方法では、値が8, 9のときに10以上になったことを検知できなくなってしまいます。かなり厄介です。

対策

これを防ぐにはどうしたら良いでしょう。値が8, 9のとき、次回繰り上げ処理をするようなプログラムを書く?いやいや、前回の演算が影響してくるループとかややこしくてやってられません。バグの元です。

こんな時はまるっきり見方を変えてみましょう。

「桁の値が5以上ならば次に左シフトすると桁あふれを起こすから、あらかじめ繰り上げ処理をしておこう」

これならどうでしょうか。
繰り上げ処理とは、10~15の6つの数字をスキップすることですから、10以上だった場合は6を足してあげれば良いのです。6を足せば自身の位の最上位bitが自然とあふれて1つ上の桁の最下位bitに1が足されます。
これを左シフトする前に行うのならば、6の代わりに3を足しておけば、シフト後に6を足したのと同じ計算結果になるはずです。

というわけで、方針がここまでで以下のように推移してきました。

×「桁の値が10以上ならば桁あふれを起こしているから、1つ上の桁に1を足して自身から10を引こう」←桁あふれを起こしても10以上にならないことがあるからダメ
△「桁の値が10以上の場合の処理に加えて、左シフト前の桁の値が8,9のときは左シフト後に桁の値が10以上にならない桁あふれを起こすから、その場合の処理を記述しよう」←正しいが実装上の条件が複雑でよくない
○「桁の値が5以上ならば次に左シフトすると桁あふれを起こすから、シフト完了後に6を足そう」←だいぶ条件がスッキリしたが、ループの前回の条件が影響するのはまだ改善の余地あり
◎「桁の値が 5以上ならば次に左シフトすると桁あふれを起こすから、あらかじめ3を足しておこう」


これに従って先ほどのBCD変換をしてみます。
 先ほどは「1001だから桁あふれしていない」、シフト後は「0011だから桁あふれしていない」という判定となり、本当は桁あふれしていることに気づかずに通り過ぎてしまっていましたが、今回は「1001は5以上だから次のシフトで桁あふれする」という判断になり3を足すことができましたので、次のシフトでBCD1の位に正しく「1001(2進数)」=「9(10進数)」が入りました。

続きを見ていきましょう。
左シフトの前には必ず各桁に対して「5以上か」という判定が入り、5以上ならば3を足すことで繰り上げ処理としています。
これで、最終的に8回シフトが終わった時点で0001 0101 0010で152が得られました。めでたしめでたし。

まとめ

かなり丁寧に書きましたが、まとめるとアルゴリズムの考え方としては以下の通りです。
  1. 2進数の各桁(=bit)を2倍しては次の桁を足して…という処理をBCD上で行うことで2進数→BCD変換を行う。
  2. 2倍する際にはBCDだと桁あふれが起こるため、分岐処理が必要。
  3. 桁あふれしてから繰り上げ処理をしようとすると条件が複雑になるので、2倍する前に桁の値が5以上だったら3を足して繰り上げ処理とする。
アルゴリズムとしてはここまでで、これをいかに効率よく処理させるかということに関しては以前の記事にしっかり載っていますので、必要な方は参照してください。

2020年5月1日金曜日

MPLAB X 5.35 + XC8 2.20でオートコンプリートが正常に動作しない問題を対処する

最近、久しぶりにPICマイコンでのプログラミングをしました。
久しぶりにするということで、ひとまず最新の環境に揃えた上でプログラミングを始めたところ、オートコンプリート機能が全く動かないというトラブルに見舞われました。
こんな感じです。ありとあらゆるレジスタの名前が未解決になってしまうため、IDEというよりかただのテキストエディタになってしまいます。この状態でプログラミングするだけでもストレスフルです。
なお、IDEのオートコンプリート機能がトラブっているだけで、コンパイル自体は正常に通ります。

環境

  • MPLAB X 5.35
  • XC8 2.20
  • PIC16F1579(確認はしていないがおそらく他の8bitファミリでも同じと思われる)

原因

結論から言うと、原因はMPLAB XがPIC16F1579.hを見失っていたからでした。そのため、各SFRの名前やアドレスが分からなくなっていたのです。

黄色い波線が引いてあるxc.hにCtrlキーを押しながらマウスオーバーするとこんなポップアップが出てきました。
xc.h内ではどうもプロジェクトの設定を元にデバイスのヘッダファイルを読み込む条件分岐があるようで、その中で「pic16f1579.h」見つけられずにレジスタ名をすべて解決できなくなっていたようです。
すでに上の画像にも書いていますが、試しに#include <pic16f1579.h>を追加してみたところ、以下のようなエラーが出てきました。

この中で赤文字の"C:\Program Files\Microchip\xc8\v2.20\pic\include\plib"を参照してみたところ、このようなフォルダはありませんでした。おそらく過去はこのようなパスにそれぞれのPICのヘッダファイルを入れていたのでしょうが、最近パスが変更された一方でMPLABのほうではその変更が漏れておりヘッダファイルを見失ってしまったのでしょう。
現在のフォルダは"C:\Program Files\Microchip\xc8\v2.20\pic\include\proc"でした。

対策

system include pathsが間違っているので、これを修正してやれば直るはずです。

…と思ったのですが、system include pathsの編集のしかたが分かりませんでした。設定にもそのような項目は見当たらず、かと言ってMPLAB Xの膨大な構成ファイルからそれが記録されている設定ファイルを探す気にもなれず、結局お手上げでした(もし知っている方がいたら教えてください)。

system include pathsの編集がダメだったので、付け焼き刃ではありますが、プロジェクト単位で"C:\Program Files\Microchip\xc8\v2.20\pic\include\proc"のパスを通してあげればpic16f1579.hが見えるようになりました。
プロジェクトのプロパティ画面のツリーの「XC8 Compiler」を選択し、「Include directories」に先ほどのパスを追加してあげます。
見事にSFRの下の波線もなくなり、オートコンプリートが正常に動作するようになりました。めでたしめでたし。


にしてもあれですね、MPLAB Xって必ず「こんなん絶対誰か気づくやろ」みたいなお粗末なバグが入ってますよね。訓練されたユーザーじゃないと使いこなせないソフトです…。

2020年4月1日水曜日

久しぶりにプログラミングをした話

別にエイプリルフールネタじゃないよ。

最近仕事が忙しかったうえ、あまりこれを作りたいというインスピレーションも無く、全然プログラミングに触れていませんでした。
前回の記事は全然プログラミングに関係ないめちゃめちゃアナログな回路のお話でした。その前はリフルシャッフルをテーマにした算数のお話で、辛うじてこじつけ程度にプログラミングに触れた程度でした。

ですが、世の中はコロナパニック、二言目には「不要不急の外出は控え」ということで、先週末久しぶりに少しだけ文字列をいじくるC#プログラムを書きました。そのところ、未来にでもワープしてきたんじゃないかというくらいめちゃめちゃ環境が変わっていてびっくりしたので、ひとまずかるーく記事にしておこうと思います。

Range構文

まあ「未来にでもワープしてきた」と大げさに書きましたが、気づいたらC#8.0がリリースされていたというだけです。
C#8.0ではRange構文というものが導入されました。これまでは配列や文字列の一部分を切り取る際に、よくこんな書き方をしていました。

text.Substring(now, m.Index - now);

now~m.Indexの範囲の文字列を抽出するだけのプログラムですね。
余談ですが、mはMatchクラスの変数です。すなわち、now以降正規表現でマッチした部位より手前の文字列を抽出するだけのプログラムです。

おやおや、VisualStudioさんに煽られてしまったではありませんか。Substringを使うのはもう時代遅れと。

実質的にこのRange構文を使ったコードはSubstringに展開されるだけのようなので、バイナリとしては変わりませんが、冗長な引き算やメソッド名が省略できるうえ、構文として意味が明確に定義される(メソッドだとメソッドを実装した人の流儀で決まってしまう)のでとても良い構文だと思います。

今回は使いませんでしたが、これ以外にも「末尾から何文字」とかいう表現も容易にできます。かゆいところに手が届く、非常に強力な構文ですね。はい。

null許容参照型

さて、Range構文を採用したとなると、セットで出てくるのがSpan<T>です。
C#が言語としての安全性を求めるあまり、C言語ではゴリゴリ書けていたポインタをいじくりまわすようなプログラムは書けないようになっています(Unsafeは除く)。ですが、その代償として場合によってはあまり効率の良いプログラムは書けないもどかしい状況がありました。

そこで、安全にポインタらしい機能を実装しようということで導入されたのがSpan<T>です。これは「範囲」を指定するRange構文と非常に相性が良く、Range構文で示した範囲をSpan<T>で参照として返す、というのが非常に基本的な使い方となります。

というわけで、text.AsSpan()してRange構文を適用しようとしたら…

ん…?「string?」 …?

型名の後ろに着く「?」は、null許容型を意味しています。
そもそもnullが無い値型をNullable<T>構造体でラッピングし、nullという意味を追加しようというものです。
ですが、stringは参照型ですので、わざわざNull許容型にしなくてもnullを取ることができます。なので、後ろに「?」を付けると昔ならばコンパイルエラーになっていました。

はい、これもnull許容参照型という C#8.0から導入された機能、「null許容参照型」です。

「null」は参照型がどこも参照していない、という意味を表す定数です。参照型には、参照である以上「どこも参照していない」という状況が付いて回るのですが、それを使うかどうかはまた別問題です。実際問題、null状態の参照型を受け付けたくない状況は非常にたくさんあります。
その場合、今までならばメソッドを実装するときに最初にパラメーターのnull性を確認して、場合によってArgumentNullExceptionを投げるようなプログラムを書くことになりますが、めんどくさいですよね。そして使う側もその例外が発生する可能性を考慮しなければならない。本来の大切なロジックを書く以前のところで雑務が発生してしまうし、コードとしてもノイズが入るので見通しが悪くなってしまうわけです。

そこで、C#8.0では、ただ単に参照型の型名を書くと「null非許容」としてみなすようになりました(破壊的仕様変更になるので、オプションでONしないと有効にはならないようです)。その場合、コンパイラが非nullであることをしっかり確認し、どこかでnullになるような可能性があればエラーを出してくれるようになりました。
逆に、nullを使いたいという時は、敢えて型名に「?」を付けなければならなくなりました。これがnull許容参照型です。

気づいたのがプログラムを書いている途中だったので、今回は先延ばしです。オプションは有効にしないことにします。
ですが、厳格に運用できれば、逆にもっと効率よくすっきりとしたプログラムを書けるようになりますね。


ほかにもC#8.0には多くの機能が実装されています。
例えばswitch式とかインターフェースのデフォルト実装機能とか。

そもそも半年前にC#8.0が正式リリースされているにもかかわらず、そのことを知ったのが今って、本当に久しぶりにC#に触ったんだなあ。また余裕ができたら何か作りたいもの考えてプログラム書かないとな。

2019年7月30日火曜日

フルアナログの自励式フライバック・コンバータを自作する

前回の記事から遅くなってしまいましたが、今度は自励式フライバック・コンバータを自作してみました。そりゃあね、原理がわかったら自分で試したくなるのがサガってもんでしょう。

注意:スイッチング電源はそのサージにより数百Vの電圧が発生する部位がありますので感電したら重大な事故になることがあります。また、状況により上手く動作せず素子が焼損したり、最悪爆発して重大な事故になる可能性もあります。危険性を認識し安全対策を行ったうえ、各自の責任で製作するようお願いします。万が一の事態が発生しても当方では一切責任を負いかねます

基本設計

まずは基本設計をします。どれくらいのスペックの電源が欲しいかを設定します。

入力電圧 AC100V
出力電圧 2~15V
出力電流 最大3A
スイッチング周波数 75kHz

こんな感じでしょうか。日本でコンセントに接続して使う感じです。出力は可変とし、3Aくらい流せるように作ります。フライバックコンバーターだと数十W程度の出力が限度と言われておりますので、まあこんなもんでしょう。
スイッチング周波数は少なくとも可聴域(~20kHz)は外すようにします。そうしないとコイル鳴きが聞こえて不快です。たいていフライバックコンバータは70~80kHz程度を狙うそうです。

トランス製作

さて、まずはトランスを自作することとします。というよりか、スイッチング電源はトランスが最も鍵となるパーツの上、使用環境によってパラメーターがごっそり変わってきます。市販品をそのまま使うことはまずできません。
となると、トランスのボビンとコアを買ってきて自分で巻かなければなりません。ですが、割とニッチな趣味なのかあまり小売りしている電子部品店は無さそうでした。比較的入手性の良さそうなのはaitendoかと思います。aitendoでは2種類のサイズを売っておりますので、大きいほうのEE25コアを採用すること前提で設計をしていきます(なんか販売終了予定になっているようですので必要な方はお急ぎを)

手順としては、基本設計パラメーターをもとにトランスを設計していきます。それをもとに実際にトランスを巻き、出来上がったトランスのパラメーターを測定します。どれくらい狙ったところにパラメーターを合わせ込めるかはわかりませんが、出来上がったトランスのパラメーターを使って回路全体を設計していくこととします。

トランス設計

aitendoにはトランスコアのデータシートはありませんが、一般的なEE25のパラメーターを使用します。

Le 48.7mm
Ae 40mm2
Bs 420mT@100℃

$L_e$は実効磁路長、$A_e$は実効断面積です。$B_s$は飽和磁束密度ですので、常にこの磁束密度は超えないように注意しなければなりません。

トランスはフライバック型ですので1次側をONしてエネルギーを溜め、その後2次側から放出するという流れになります。そのほか、スイッチング制御用に補助巻線を用意します。
電流はこの図のように流します。$D_{on}$はトランジスタをONしているデューティー比、$D_{off}$はトランジスタをOFFしているタイミング、すなわち2次側にエネルギーを放出しているときのデューティー比です。$D_{delay}$はスナバコンデンサとトランスが共振してスナバコンデンサの電圧が最小になるまで待つ時間です。これを挿入することによりスナバコンデンサの電荷を回生できるので効率が上がります。

1次側

1次電圧は最小で102Vとしました。深い意味はありません。理想は$100{\rm V}×\sqrt 2 =141{\rm V}$となりますが、実際はダイオードブリッジのドロップだったり、平滑リプルによる実効値の低下だったりいろいろあります。ですので、$85{\rm V}\times0.85\times \sqrt 2$にしてみました。

すると、出力は最大で15V3Aに設定しているので出力は45W、効率を仮に82%と置いてみると入力側の電力は55W必要となります。ですので、入力の電流は0.54Aとなります。
その入力電流は平均値です。フライバックコンバータは、実際はトランジスタがONしている間に電流が時間に比例して増えていき、OFFしている間はゼロとなります。ですので、電流の最大値$I_{peak}$は以下の式で表せます。\[I_{peak}=I_{ave} \times \frac{1}{2D_{on}}\]分母に2が付いているのはノコギリ波だからですね。
$D_{on}=0.5$としていますので、$I_{peak}=2.15{\rm A}$となります。

さて、電圧と電流が定まると1次側の自己インダクタンスを求めることができます。
スイッチング周波数を75kHz、デューティー比を0.5からON時間は6.67usとなります。この時間で電流が0から2.15Aまで増えるときにトランス両端に102Vが生まれるわけですから、インダクタの基本式\[V=L \frac{{\rm d}I}{\rm dt}\]を用いてLは317.1uHとなります。

一方で、巻き線のターン数はファラデーの法則スタートで計算できます。\[V=-N\frac{{\rm d}\Phi}{\rm dt}\]コアの最大磁束密度420mTより、安全率0.72を取って最大の磁束密度を302mTとします。そうすると、ON時間(=6.67us)間にこれだけの磁束の変化があればいいわけですから、代入して1次側巻き数を$N_p=56$と求めることができます。

最後にギャップ長を求めておきます。トランスにギャップを付けるとギャップが磁気抵抗のほぼすべてと近似できるため、以下の式が成り立ちます。\[\mathcal R_m=\frac{l_g}{\mu_0 A_e}\]これと磁気抵抗の公式$\mathcal R_m=N^2/L$からギャップを求めると0.497mmとなります。これは実効磁路長におけるトータルのギャップ長ということに注意してください。EE25コアを浮かせると3か所にギャップができますので、1つ当たり1/3でだいた0.165mmとなります。ギャップ長はだいたい0.5mm以下程度を目安とするといいでしょう。あまり大きくなりすぎると漏れ磁束が大きくなり、結合度が下がるだけでなく、サージも大きくなってしまいます。

2次側

トランス2次側は平均値として3Aを出力しなければなりません。トランスから2次側にエネルギーを放出している時間のデューティー比を0.4とし、トランスの出力波形がノコギリ波であることを踏まえると$I_{peak}=3\times2/0.4=15{\rm A}$となります。
2次側自己インダクタンス$L_s$は1次側と同様に求め、5.55uHとなります。ただし、電源装置として出力15Vを確保するために、トランスの出力としては整流ダイオードのドロップ分を考慮して15.6Vとして計算しています。

トランスは1次側と2次側の巻線比の2乗がインダクタンス比ですから、それをもとに2次側の巻き数を求めると7ターンとなります。

補助巻線

補助巻線は1次巻線と巻線の向きが同じですから、1次側との電圧比が巻線比となります。10V出力とすると6ターンとなります。自己インダクタンスは4.08uHとなります。

文章と数式だけで長々と書いてしまいましたが、まとめると以下のようなパラメーターとなります。

1次 自己インダクタンス 317.1uH
巻数 56
2次 自己インダクタンス 5.547uH
巻数 7
補助 自己インダクタンス 4.075uH
巻数 6

トランス巻き

つづいてトランスを巻いていきます。
巻き終わりました(((
結合度を上げるために、1次巻線を半分巻いたら2次巻線を巻き、その上に1次巻線を巻くといった巻き方をしています。
最後は自己インダクタンスを測定しながら上記の値になるようにギャップを調整して終了です。

パラメーター測定

最後に、後ほどシミュレーションできるようにしっかりパラメーターを確認しておきます。各端子の自己インダクタンスのほか、他の端子を短絡させたうえでのインダクタンスも測定しておきます。それにより、結合部分の自己インダクタンスがゼロとなるので、漏れ磁束による自己インダクタンスを求めることができます。そこから巻線間の結合度も導くことができますので、のちのシミュレーションに非常に役に立ちます。
今回私が巻いたものの測定結果は以下の通りになりました。

自己インダクタンス 1次 329.9uH
2次 5.231uH
補助 4.022uH
結合度 1次—2次 0.9865
2次—補助 0.9208
補助—1次 0.9383

ちゃんとした商品の電源トランスは結合度0.9999とか平気で超えるそうですね。ただ、今回はギャップがあったり自分の工作精度だったりでここまで値が下がっています。1次—2次は交互に巻いたので結合度が高いですが、その上に補助巻線を巻いたので、補助巻線と他の巻線の結合度は1次―2次間に比べて悪くなってしまっております。

絶縁抵抗計などを持っていれば1次側と2次側の絶縁性を確認しておくのも良いでしょう。私はあの2万円くらいする計測器は買えませんでした…。

回路設計(シミュレーション)

回路は上記のトランスをもとに作っていきます。

上の画像はシミュレーション用に作ったLTSpiceの回路です。良い部品がなかった場合は類似品としているので、完璧にシミュレーションできているわけではない点に注意してください。
メインのスイッチング用FETは東芝製のTK20A60Uとしました。耐圧600Vと比較的高く、ON抵抗も0.165Ωとかなり抑えられています。

C6とC3は部品選定がなかなか重要です。整流平滑用コンデンサはスイッチング周波数で充放電が繰り返されますので、かなり激しく電流が行き来し、コンデンサの内部抵抗で発熱をします。そのため、それに耐えられるかどうかの指標として「許容リプル電流」というものが定められており、これが使用する回路に適合するように選んでいく必要があります。
とはいえ、リプル電流の実効値を計算していくのはなかなか難しいです。それだけで記事が一つできてしまいそうですので、皆さん各自勉強してください(私ももうちょっとちゃんと勉強したいです)。
今回はC6はニチコンLSシリーズの250V220uF(リプル電流1260mA)、C3はニチコンVRシリーズの25V2200uF(リプル電流1550mA)の2並列としました。回路の中でも大型部品となりますので、箱にちゃんと納められるのかという意味での制約も出てきて選定になかなか苦労します。

他は敢えて説明するほどのものでもないかもしれませんが、少しだけ説明しておきます。C1はM1のゲートを駆動するためのコンデンサです。前回の記事ではバイポーラでしたが、今回はMOSFETですので電荷の供給元が必要です。Q2はフォトカプラだけだとゲインが足りなかったので付けました。なかなかこの辺の回路構成は試行錯誤でした。

あとは、自分の求めたい出力電圧や出力電流を設定しながら、ひたすらトライアンドエラーで回路パラメーターを調整していきます。上手くしないと変に共振してトランス1次側のサージ電圧が高くなってしまったり、2次側の出力電流が大きくなりすぎてトランスが磁気飽和するレベルになってしまったりしますので、実際に作ったときに事故を起こさないようにあらかじめシミュレーションを追い込んでおきましょう。


シミュレーション結果はこんな感じになりました。15V3A出力で安定して出力できています。トランジスタのドレイン電圧も定格はオーバーせず、電流も飽和しないレベルです。

ある程度回路シミュレーションが追い込めたら、次は試作を行いましょう。今回の記事では省略しますが、実際シミュレーション通りに動くとは限りませんからね。
私はそこでトランジスタをいくつか焼いています。

基板設計

さて、試作も終わりある程度作れる目星がついてきたら次は基板を起こします。
回路は基本的にシミュレーションと同じですが、部品単位で起こしたので少し見た目が変わっているのと、ソフトが違うので部品番号が変わってしまっています。また、シャントレギュレーターはシミュレーションではTL431を使いましたが、試作したところおそらく位相余裕が無く負荷条件によっては発散してしまうようで、ここでは製品として高周波ゲインが抑えられた新日本無線のNJM2825を使いました。これだと最小1.2Vまで下げられますし、今回の可変用途にもうってつけです。

さて、今回はKiCadで基板を起こしてみました。


J2はパイロットランプ用の出力、J4は電圧調整用ボリュームのコネクタです。
余談ですが、NJM2825はカソード側が基準となっているオペアンプのため、Bカーブのボリュームの角度に比例した電圧を出力させようとするとこのような位置にボリュームを設置せざるをえなくなってしまいました。この場合、J4を差し忘れたり接触不良が起こった場合、出力電圧が最大値まで跳ね上がってしまうフェイルセーフではない設計となってしまっています。あまりそういうのは良くないでしょうが、かと言ってR15の位置にボリュームを付けると電源装置として使いにくくなってしまいますので、悩んだ末に安全を捨てました。安全を捨てたといっても、コネクタを差しさえすれば問題は無いですので、ある日突然…ということはまあまず無いでしょう。

また、コンセントからアース端子を基板に取り込めるようにしており、出力側はC9を介してアースに接続しております。この回路図には出てきていませんが、筐体に取り付けたACインレットはノイズフィルタ付きの製品を使っておりますので、厳密には1次側と2次側が絶縁された回路と言うわけではなくなってしまいます。
ただ、2次側の出力が完全に浮いているような設計にしてしまっても、回路である以上何かしらの容量で結合しています。そのようなよくわからない電位を設定するくらいなら、ちゃんとマイナス側をアースにコンデンサ結合させておいたほうが安心ですし安定もするはずです。



基板はこのような感じになりました。
基本的には1次側と2次側は分離しております。中央少し左のトランスとフォトカプラを境に1次側と2次側が分かれております。唯一茶色のレイヤーでそこをまたいでいるベタパターンがアースです。
スイッチング回路はノイズを出しますし、他から受けても困りますので、できるだけ機能ごとにまとめます。スイッチング制御回路は中央下側、フィードバック回路は左下側にまとめています。
スイッチング用トランジスタと出力の整流ダイオードは熱を発しますので、放熱板を設置できるような位置に配置します。また、電解コンデンサは熱でどんどん寿命が縮んでいくため発熱部品からは離して配置し…たいところなのですが、そうはいきませんでした…。

それにしても、KiCadの3D表示機能は圧巻ですね。発注した基板が到着するのがいつもより待ち遠しくなってきます。

製作

基板が届いたら基板への部品の実装と箱の加工を進めていきます。
今回、箱はタカチのTS-1というアルミケースを使ってみました。結構ギリギリでしたが、まあ何とか納めることができました。
それにしても、なかなか金属加工というものは骨が折れるものです。できれば工作機械でパパッと穴を開けたいところでしたね。そんないいもの私は持っていないので、ドリルと金ノコ、テーパーリーマとやすりでひたすら頑張りました。

先ほど少し言いましたが、ACの入力側にはノイズフィルタ付きのインレットを使っております。スイッチング電源は高周波で動作しますので、家庭のコンセント側に高周波ノイズをばらまいてしまわないようにとの配慮です。
基板からインレットまでの配線もできるだけツイストし、その配線から出る輻射ノイズもできるだけ抑えるようにしてあげましょう。
動作確認というほどたいそうなことでもありませんが、つまみを動かしたり適当な負荷をぶら下げてみて動作確認をします。
シミュレーションでは15V3A流れましたが、5Ω負荷をぶら下げると5V程度で出力がサチってしまいました。なかなか計算通りにはいかないものです。こういう時に電子負荷装置でも持っていると、負荷を変えながら特性を詳細に探ることができるのでしょうが。

調整

さて、電流が思ったほど出なかったので現物合わせでパラメーター調整をしていきます。
R5でC3を充電し、Q1のベース電圧が上がるとMOSFETをOFFする、という流れになりますので、充電時間を長くすればQ1がなかなかMOSFETをOFFしに行ってくれないのでたくさんの電力を取り出せるようになるはずです。
というわけで、R5を増やしていきました。増やしていったところ、7.5kΩにて短絡電流が3A程度になったので、ここで調整終了としました。 このように複雑なアナログ回路は計算通りに動いてくれないので、最後は現物合わせという形を取る必要が出てきます。

5Ω負荷で1.7A出力ですので、当初予定ほどの性能は出ませんでした、でも、短絡時にメーターが振り切れても困るのでまあこんなところで良いでしょう。これくらいの出力が取れれば、まあ趣味の電子工作程度ではそうそう困ることは無いかと思いますし。

おまけ


消費電力計を買ってきて作ったスイッチング電源の効率を測ってみました。

もちろん負荷によって変わりますが、高出力状態の時でおおよそ75%程度です。もうちょい、実用的には8割以上は行きたいところですが、まあ適当設計なのでこんなもんでしょう。
力率は0.57程度と低めです。というのも、スイッチング電源は入力段にダイオードブリッジとコンデンサでの整流回路を備えていますが、そのような整流回路は電圧がピークになった周辺でしか電流が流れないため電流波形が大きく崩れます。そのため力率は低く、一般的にこれくらいになるそうです。
フライバックコンバータ程度だと問題ありませんが、比較的大きな負荷だと電力系統にダメージを与えてしまいますので、整流回路を制御して力率を改善する回路を入れたりするそうです。


また、出力が2V前後では結構出力リプルが大きくなってしまいました。これはまあフィードバック回路の性能ですので、シャントレギュレーター周りのフィルタを調整することで改善できるのかもしれませんが、基板パターンを改造しなければならないので今回は現状とします。なかなか広い範囲で出力電圧を安定させるというのは難しいのですね。

まとめ

さて、フルアナログのフライバックコンバータ型スイッチング電源を自作してみました。トランスの製作、回路の設計から実際の電源装置の製作までやり、計画していた性能には届きませんでしたが、なんとか実用レベルのものを作りあげることができました。
1個数百円するトランジスタを何個も焼きながら試行錯誤して、結構いろいろな勉強になりました。普段パソコンとかマイコンとかでプログラミングばっかりやっているときとは違う頭の使い方をしますし、アナログ回路って本当に生身で物理の世界に挑んでいる感じがありますのでまた別の面白さがあると思います。

さて、次は何作ろうかな。

2019年5月18日土曜日

フルディスクリートのフライバック・コンバータを解剖する

最近プログラミングらしいプログラミングのネタが無いので、もう割り切って一切プログラミング関係ないことを記事にしようと思います。

この前とある部品屋に行ったところ、900円の特価でスイッチング電源が売っていました。しかも基板を見るとICらしいICが付いていません。これは買っていろいろ調べるしかない…!
※買った数週間後、別のパーツ屋に行ったら500円で売ってました。ショック…。


というわけで買ってきました。
先に結論を書いてしまいますが、回路図はこのようになっておりました。


順に解剖していきます。

トランスの基本

さて、どこまで基本的なことを説明するか、という話になってしまいますが、トランスについて「1次側と2次側の巻数比で電圧が変換され、1次側から2次側へ電力が伝わる」程度の理解しかしていない人のために少しだけ原理的な話をしておきます。

トランスは同じコアの上に複数の巻線を巻いたものです。そのため、コア内の磁束を$\Phi$とすると巻線の両端の電圧は次式で表せます。\[V=-N\frac{d\Phi}{dt}\]何を迷うこともない、ただのファラデーの法則です。
ここで重要なのは、コアを共用しているため、トランスの全ての巻線はいつどの瞬間でも巻線比に応じた電圧が発生するということです。交流/直流とか正弦波/矩形波とか巻線に流れている電流とか何も関係ありません。コアの磁束の変化はどの巻線にとっても同じなので、理論上は巻線比$N$のみで電圧が決まります
※磁束の漏れとか巻線抵抗とかそういう話をしだすとその理論上の話からはずれていきますが、それは基本をマスターした人が考えてください。

さて、ではその磁束はどうやって生まれるのという話ですが、こちらはアンペールの法則になります。\[\oint_C \mathbf{H} \cdot d\mathbf{l}=I\]電流の周りには磁界が生まれるという法則です。
トランスのコア内の磁束を具体的に計算するのならばアンペールの法則と等価なビオ・サバールの法則を使うと良いでしょう。ただ、ここではあまり具体的な数字には興味が無いので計算は省略します。大事なのは「コイルに電流が流れると、それに比例した大きさの磁束がコア内に生まれる」ということです。言い方を変えれば「コア内に磁束があるということは巻線に電流が流れている」と言えます。
※アンペールの法則では「電流に比例した磁界が生まれる」とまでしか言っていません。磁界は通る材料の透磁率に比例した磁束密度を生み出し、磁束密度に断面積をかけると磁束となります。これらが合わさって初めて「トランスのコア内には電流に比例した磁束が生まれる」という説明になります。

ですので、トランスの1次側に電圧を印加した際の物理としては、
  1. 1次巻線に電圧が加わる 
  2. トランスのコア内に1次電圧に対応した磁束の変化が必要となる
  3. 1次巻線の電流が変化することで磁束の変化が発生する
  4. 2次巻線にはコアの磁束の変化に応じた電圧が発生する
  5. 2次側の負荷によって電流が流れた場合、その電流に応じた磁束がコア内に発生する
  6. 2次電流による磁束の変化によって1次電圧が変化するのは不都合(電圧源につながっている)なので、その磁束変化を打ち消す分の電流が1次側に流れる
という流れとなります(言葉での説明ですので受け取り方によっては多少因果関係がおかしく感じるかもしれません。気になった方は計算して納得してください)。

なお、この説明まででは直流でもトランスで電圧が変換できそうに見えてしまうと思います。実際ここまででの説明ではできてしまいます(電流はべらぼうに流れますが)。しかし実際はできません。
足りていないお話は「磁気飽和」です。電流が磁界を生み、磁界が磁性体の中を通ると磁束密度が生まれます。しかし、磁束密度の上限は材料によって決まっているのです。それより先はいくら磁界が大きくなっても磁束密度は増えません。すなわち、電流がある一定に達すると、それ以上電流が増えてもコイル両端の電圧が上がらなくなるどころかゼロになってしまうのです。ですので、トランスは磁気飽和が起こらない範囲で電流を調整しながら使わなければなりません。

フライバック・コンバータ

ところで、スイッチング電源と一口に言ってもいろいろな方式があります。今回買ってきたスイッチング電源はいわゆる「フライバック・コンバータ」と呼ばれる形式のスイッチング電源になります。比較的シンプルな回路で部品点数も少なくなり、数十Wクラスまでならこの方法で充分に対応できるため、電子工作レベルのスイッチング電源には多用されています。

主回路は次のような構成になっています。



トランスの●は極性を示すものですね。フライバックコンバータでは1次側と2次側で必ず逆極性となります。そのため、1次側に電流が流れているときに2次側に電流が流れるわけではないということに注意してください。


まずはトランジスタをONしたときです。トランス1次側には電源電圧と同じ電圧vがかかります。と同時に2次側にも巻線比nを掛けた電圧nvが出力されますが、ダイオードが付いているため電流が一切流れません。
となると、トランスはただのインダクタとして機能し、1次側の自己インダクタンスに従って線形的に電流が増えていきます。この時流れた電流はトランスのコアに磁気エネルギーとして蓄えられます。


続いてトランジスタがOFFすると1次側では電流が一切流れなくなります。しかし、磁束は連続的にしか変化できないので、2次側に電流を流すことでコアの磁束の変化を連続的にしようとします。すなわち、コアに蓄えられた磁気エネルギーが2次側に電流として放出されるのです。放出された電流はダイオードを通ってコンデンサに充電されます。


タイムチャートにするとこんな感じです。青が1次側、橙が2次側です。シンプルでわかりやすいですね。

発振回路

さて、フライバック・コンバータを実現するには、トランジスタを周期的にON/OFFしなければなりません。そのためには発振回路が必要となります。
複雑な回路やプログラムを持ったICやマイコンを使っても良いのですが(モダンな電源で高効率化を目指すのならばそういうのが必要なのでしょうが)、ここは基本的なアナログ回路での発振を実現しています。発振回路の基本は正帰還となります。


トランスが3巻線になりました。1次・2次でないものを「補助巻線」と定義します。
トランジスタがONしているとき、トランスの1次巻線には電源電圧が印加されます。そうすると補助巻線にも電圧が発生します。電圧が発生するとトランジスタのベースに電流が流れるためさらに1次巻線に電流を流す方向へ進みます。まさに正帰還ですね。
しかし、1次巻線に電圧がかかると電流は線形的に伸びていきます。一方で1次巻線の電圧は一定なので補助巻線に生まれる電圧も一定になります。そのためベース電流も一定です。バイポーラトランジスタはベース電流に対してコレクタ電流の流れる電流が決まりますから、いつか1次巻線の電流がコレクタ電流の上限に到達し、電流が制限されてしまいます。電流が上限に達し一定になると、コイルに流れる電流が変化しなくなりますから1次電圧が急激に下がります。そうすると補助巻線の電圧も下がり、トランジスタも急激にOFFします。まさに正帰還です。

ただ、これだけでは最初にトランジスタをONすることができません。
ですので、ONしてくれる程度の大きな抵抗を電源側に付けてあげます。


これで起動もバッチリ、後は上記の正帰還によって発振を続けていきます。

フィードバック回路

さて、これで発振しても出力電圧を安定化できません。毎周期ごとにトランスに蓄えたエネルギーを2次側のコンデンサに移すだけですので、負荷が無ければどんどんコンデンサの電圧は上がっていきますし、負荷があれば下がります。
すなわち、出力電圧を監視してスイッチングを止めたり動かしたりする必要が出てきます。


その回路を追加したのがこれになります。
トランスを使った電源回路の1つの大きなメリットとして、入力と出力が絶縁されるということが挙げられますが、そのメリットを生かしつつ出力を制御にフィードバックするにはフォトカプラを使います。
そのフォトカプラの手前にはシャントレギュレーターを設置しており、ここで出力が基準電圧より高いか低いかを判断します。基準電圧より高くなるとカソードーアノード間が通になり、フォトカプラが光ります。
フォトカプラが光ると今度は補助巻線に電圧が発生しているときにフォトカプラを経由して補助トランジスタのベースに電流が流れ、主トランジスタのベース電流を補助トランジスタで吸収してしまいます。そうすると主トランジスタがすぐさまOFFし、2次側へエネルギーを供給する量が減ります。これによって出力電圧が下がるのです。
出力電圧が下がり、フォトカプラがOFFするとまた通常のスイッチング動作が始まり、2次側へエネルギーを供給する状態になります。

細かい回路の工夫

ここまでが回路の基本的な動作の仕組みとなります。
ただ、これだけでは充分ではなかったり、逆に効率を上げるために他の工夫をしなければならなかったりして、さらに一ひねり二ひねりある回路を付け加えています。

共振によるサージ吸収

フライバック・コンバータの動作説明で「1次側の電流を瞬間的に切ることによって、磁束の連続性を維持するために2次側に電流が生まれる」と説明をしました。
理想的なトランスならばそれで問題ないのですが、現実はそう甘くありません。

トランスには「漏れインダクタンス」というものがあります。巻線が作る磁界がすべてコアの中を通ってくれれば良いのですが、どうしても一部漏れてしまいます。その漏れが他の巻線に入らなければ、ただの自己インダクタンスとなってしまいます。
自己インダクタンスということは、何が何でもその分の磁束は電流遮断時に自分で賄う必要があります。となると、それによって何とか電流を流そうとトランス1次側に非常に高い電圧が発生してしまいます。サージです。

スイッチング電源のサージ吸収はスナバ回路と呼ばれる回路で吸収することが多いのですが、そうするとそれがそのまま熱として捨てることになってしまいます。ちょっともったいないです。そこで、トランジスタに並列にコンデンサを入れることで、ここで自己インダクタンスに溜まっていたエネルギーを吸収することができ、さらにはトランスの起電力が無くなった後(=2次側からエネルギーを抜き取った後)に整流コンデンサにエネルギーを回生できます。



スイッチングのデューティー比調整

先の「発振回路」のところでフライバック・コンバータは補助巻線がそもそも正帰還なのでそれだけで発振できると書きました。しかし、それだけでは発振のパラメーター調整が難しいです。
そもそもフライバック・コンバータの発振におけるデューティー比は生命線です。ON時間が長すぎたりOFF時間が短すぎるとトランスが磁気飽和を起こして事故が起こりますし、ON時間が短かったりOFF時間が長いとエネルギーを効率的に2次側へ移せません。それを主トランジスタの飽和電流だけで制御するのは流石に無理があります。
また、上記の共振回路によるエネルギーの回生では、共振コンデンサのエネルギーが抜けきったタイミングを見てトランジスタをONしなければなりません。かなり高度なタイミング制御が必要なのです。

そこで、主トランジスタの飽和電流だけでスイッチング周期を決めるようなことは通常しません。万が一電流が流れ過ぎたとき(=磁気飽和する一歩手前の状態になったとき)に止めるための安全装置として機能します。
普段は副トランジスタがスイッチングのタイミングを決めることになります。


まず主トランジスタがONしているときですが、補助巻線は●側が+になりますので副トランジスタのベース手前のコンデンサがどんどん充電されていきます。そして、副トランジスタのしきい値電圧に達したときに主トランジスタはOFFします。


主トランジスタがOFFすると全ての巻線には逆起電力が発生しますので、今度は補助巻き線の●側が-になり、副巻線のコンデンサを放電していきます。コンデンサが放電されるとまた主トランジスタはONし、この動作を繰り返していきます。

ON時間とOFF時間を変えるために、間にはツェナーダイオードが入っています。ツェナーダイオードは順方向ではただのダイオード、逆方向ではツェナー電圧まで電流を流しませんので、ざっくり電流の向きによって抵抗が異なる素子として振る舞います。そのため、充電と放電の時間を変えることができるのです。

フィードバックの位相補償

出力電圧のフィードバック回路ですが、こちらもフィードバック回路ですので発振には注意しなければなりません。
位相補償はC61が担当しています。この容量を小さくすると負荷の変動に対する応答は早くなりますが、開ループ利得があるうちに位相が180°を超えてしまい発振します。大きくし過ぎると負荷変動に対する応答が遅くなります。

負荷調整

フィードバック回路が用意されているとはいえ、補助巻線に起電力が生じなくなった時点で起動抵抗を介して主トランジスタがONしてしまいます。主トランジスタがONすると補助巻き線に起電力が生じ、出力電圧が過剰ならばスイッチングを止めてくれます。
しかし、このサイクルの間にやはり少しのエネルギー移動が発生します。
この際、負荷がほとんど無いと全然そのエネルギーを消化できずにどんどんコンデンサに溜まっていきます。最もスイッチングがされていない条件でもエネルギー供給量が多すぎるのです。これは出力過電圧を起こしますので、割と忌々しき問題です。

上のスイッチング電源では、そのため68Ωという比較的小さな抵抗を出力段に入れています。ここに常時100mA弱の電流を流し、アイドル状態で伝わるエネルギーを消費し過電圧にならないように調整しているのです。
負荷が予めはっきりしていればこの抵抗は不要という判断もできますが、無負荷で使えない電源というのも分が悪いですね…。

その他

出力段のコイルはリプル調整用とか、入力段のコイルはコモンモードノイズフィルタとか、その辺の話は良いですよね。気になる人は自分で調べてください。

まとめ

フルディスクリートのスイッチング電源の回路を調べ、紐解いていきました。
特別なICを積まなくてもLCRといくつかの能動素子のみでスイッチング電源が作れてしまうことがわかりました。面白いですね。と同時に、そのようなシンプルな回路でも安全かつ効率よく動くために様々な工夫が凝らされていることもわかりました。

ここまで回路を見ると自分でも作ってみたくなっちゃいますよね!
スイッチング電源用のトランスは市販されていない(回路によって必要なパラメーターが大きく変わるため汎用品として売れない)ため自分で設計して、それに応じて回路のパラメーターを調整していく必要があります。もちろん部品の入手性の問題も絡んできますし、定性的に原理を理解しても、実際に回路パラメーターをどうするのかというのは一筋縄では決められません。
ある程度は計算で方針を決められても、その次にはコンピューターで回路シミュレーションをしなければなりませんし、それができれば次は試作をし、パラメーターの最終調整をしていかなければなりません。

その辺の話はまた別の機会に。

2019年2月13日水曜日

リフルシャッフルで元に戻る回数

「52枚のトランプに対して正確なリフルシャッフルをすると8回で元に戻る」——どこかで聞いたことがあるようなフレーズです。私は昔何かのテレビ番組で見たような記憶があります。

リフルシャッフルとは、カードを2山に分けてパラパラと1枚ずつ交互に重ねていくシャッフル方法です。マジシャンとかがやると非常にかっこよく見えるシャッフル方法ですが、私はうまくできません。2枚以上のダマが入り込んでしまいます。

実はこれ、「シャッフル」と名が付いていますが全然混ざっていないんですよね。正確なリフルシャッフルは元のカード束に対して一意の結果が出てくるのはもちろんのこと、同じ側から2枚以上連続して入ってしまう下手なリフルシャッフルでも最悪ケースで並び順は$2^{N-1}$通りしかありません。完全にランダムなシャッフルの場合は$N!$通りですからリフルシャッフルのほうがはるかに混ざっていません。52枚のトランプを想定すると、リフルシャッフルでは$2.25\times10^{15}$通り、完全にランダムなシャッフルでは$8.07\times10^{67}$通りですから一目瞭然ですね。
ですので、リフルシャッフルを利用するマジックは、たいていこの「実はよく混ざっていない」を使っているんですね。Norman L. Gilbreathがギルブレスの原理として数学的に体系化しているらしいです。観客に対して混ざったと思わせるには十分大きいパターンがありますが、マジックにするには十分の混ざらなさ具合というわけですね。

さて、話を戻します。52枚のトランプでは8回正確なリフルシャッフルをすると元の配列に戻ります。これを証明していきます。

$0,1,2,3,...,24,25,26,27,28,...,50,51$の番号が付いたカードがこの順に並んでいるとします。これを1回正確なリフルシャッフルをすると、それぞれのカードは$0,2,4,8,...,48,50,1,3,5,...,49,51$番目へ移動します。カードの番号がこの順に並び変わるのではなく、それぞれのカードが何番目へ移動するかを表した数列ですので注意してください。
これは、最後の51番目のカードを除いて、$k$番目なら$2k$を51で割った番目へ遷移していることがわかります。これを$R(k)$と表すとすると、\[R(k)\equiv2k\pmod{51}\]と書けます。
さらに、2回正確なリフルシャッフルをすると$R(R(k))$へ遷移することになりますので、これを$R^2(k)$と表すとすると$R^n(k)$が定義できます。\[R^n(k)=R(R^{n-1}(k))\equiv 2R^{n-1}(k)\pmod{51}\]と書けますので、再帰的に適用すれば\[R^n(k)\equiv 2^nk\pmod{51}\]となります。非常にシンプルな式ですね。
カードの配列が元に戻ると言うことは、$0\leqq k<51$の全ての整数$k$に対して\[k\equiv 2^nk\pmod{51}\]が成立するということになります。もうちょっとわかりやすく式変形すれば\[2^n-1\equiv 0\pmod{51}\]になります。
ちなみに、一番下の51番目のカードを除いて計算してきましたが、一番下のカードは毎回一番下から動かないので、考慮に入れなくても何も問題はありません。
この式が成立する最小の$n$は8ですね($2^8-1=255$で$255\div51=5$ですね)。

さて、証明できましためでたしめでたし。
ただ、これだけだとプログラミング関係のブログというテーマに反してしまいますので、無理やりにでもプログラミング要素を入れていきます。

52枚のトランプだけでなく、$2m$枚のカード束について考えていきます。
52枚でしたので$\bmod{51}$でしたが、$2m$枚なら$\bmod{2m-1}$にすればいいだけですね。
static int ReturnToOriginalCount(int CardCount)
{
    if(CardCount <= 0) throw new ArgumentOutOfRangeException(nameof(CardCount));
    if((CardCount & 1) != 0) throw new ArgumentException($"{nameof(CardCount)} must be even.");

    BigInteger exp = 1;
    int i = 0;
    do {
        exp <<= 1;
        i++;
    } while((exp - 1) % (CardCount - 1) != 0);

    return i;
}
これでカード枚数から何回で元に戻るかの関数が完成です。ちなみに2のべき乗はシフト演算で実現しています。これを使って100枚以下の枚数のカード束で何回で元に戻るかを計算するとこうなります。
N [枚] k [回]
2 1
4 2
6 4
8 3
10 6
12 10
14 12
16 4
18 8
20 18
22 6
24 11
26 20
28 18
30 28
32 5
34 10
36 12
38 36
40 12
42 20
44 14
46 12
48 23
50 21
52 8
54 52
56 20
58 18
60 58
62 60
64 6
66 12
68 66
70 22
72 35
74 9
76 20
78 30
80 39
82 54
84 82
86 8
88 28
90 11
92 12
94 10
96 36
98 48
100 30
52枚は周辺の枚数に比べて突出して少ないですね。トランプの52枚というのはなかなか神秘的な枚数です。ちなみにジョーカーを含めて54枚とすると52回も正確なリフルシャッフルをしなければ元に戻りません。
ちなみに元の式を見てもらえばわかりますが、$2^n$枚のカードのときは$n$回で元に戻ります。これも突出して周囲より低い回数で元に戻る枚数ですね。

せっかくなので、ついでにk/Nも計算してみます。数学的な意味はともかく、「枚数の割には早い回数で元の並びに戻る枚数」と考えることができます。
3万枚以下でk/Nが小さい順に並べると、トップ10は次のようになります。
N [枚] k [回] k/N
29128 18 0.000617962
21846 16 0.0007324
25576 20 0.000781983
28680 24 0.00083682
28198 24 0.000851124
16384 14 0.000854492
25306 24 0.000948392
27306 28 0.001025416
23206 24 0.001034215
19066 20 0.001048988
この中で$2^n$枚なのは16384枚だけですね。29128枚もあるカードでも、18回正確なリフルシャッフルをするだけで元に戻ってしまうんですね。
誰かやってみてください…。

2018年9月24日月曜日

Synology NAS上にGitサーバーを立ててVisual Studio 2017からpushする

半年くらい前にSynologyという会社のNASを買いました。

NASと言ったらBuffaloのイメージが強かったんですが、このSynology NASはもうほとんどただのLinuxパソコンのようで、Webからログインするとデスクトップのような画面が表示されて、そこでパソコンを操作するかのようにNASを管理することができます。また、いろいろなアプリケーションがパッケージセンターに公開されていて、それらをインストールすることで自分好みのNASにしていくことができます。例えばDropboxのように自分の各PC間でデータを同期させたり…。非常に強力なNASで、もはやただのNASというより「自前のクラウド」と言ったほうが良いような代物でした。

ですので、ただのNASとして使って、ひたすらファイルを詰め込むだけじゃなんかもったいないなと思い、Gitサーバーを立ててみることにしました。
自分でしか使わないのにわざわざGitサーバーを立てる意味とは…

準備

  • Visual Studio 2017をインストールした環境を2つ以上
  • 各PCにGitクライアントをインストール
  • Synology NASを使えるようにする

NASの準備

 まずはパッケージセンターからGitをインストールします。

次はSSHを有効にしておきます。コントロールパネルの「端末とSNMP」から有効にします。
この設定画面に書いてありますが、Synology NASはadministratorグループに属するアカウントでしかSSH接続できません。何このクソ仕様…。


そしたら次はGit用の共有フォルダを作りましょう。名前は何でもいいですが、今回はGitRemoteにしてみました。


続いてGitアクセス用のユーザーグループを作ります。コントロールパネル→グループで作成します。今回は「gitgroup」にしました。なんでもいいですが、小文字のほうがセンスがあります。
もちろんアクセス権限はGitRemoteフォルダのみに限定して、読み書き両方の権限を与えます。

つづいてユーザーを作ります。今回は「gituser」にしました。例によってなんでもいいですが、小文字のほうがセンスがあります。

グループはgitgroupのほか、administratorに所属させる必要があります。これは外部からSSH接続するためです。

administratorだとデフォルトですべてのフォルダにアクセスできてしまいますので、余計なフォルダへのアクセス権は消しておきましょう。
まあ、administratorなので自身のアクセス権の変更もできてしまうのですが…。


最後にGitを起動し、gituserにアクセス権を与えて準備完了です。

Gitリポジトリの作成

つづいてリポジトリを作成します。
SSHでNASに接続して操作をします。ターミナルはSSHがつながればなんでもいいですが、今回はPuTTYを使いました。

ログインしたら作成した共有フォルダ(/volume1/GitRemote)に移動し、リポジトリとなるフォルダを作ります。今回は「TestProject.git」としました。そして、
git --bare init --shared TestProject.git
を実行することで空のGitリポジトリを作成します。

このままでは全ユーザーにアクセス権が与えられてしまいますので、gitgroupに限定しましょう。
chown -R gituser:gitgroup TestProject.git/
chmod -R 770 TestProject.git/ 
これでグループ内にのみrwxのアクセスが与えられました。
 
これでGitリポジトリの作成は完了です。


Visual Studioでプロジェクトを作る

Visual Studioでプロジェクトを作ります。新しいプロジェクトを作る際に「新しいGitリポジトリの作成」にチェックを入れるのを忘れないようにしてください。

プロジェクトが出来上がったら、「チームエクスプローラー」→「設定」→リポジトリの設定」からリモートの「追加」ボタンを押します。
リモート名はorigin、フェッチ/プッシュはリポジトリのURLを指定します。今回は「ssh://gituser@[NASのIPアドレス]/volume1/GitRemote/TestProject.git」になります。

つづいて「チームエクスプローラー」→「同期」を開きます。
ここにある「プッシュ」ボタンを押せば、先ほど指定したリポジトリにプッシュを行います。

Gitのパスワード入力画面が開くので、ここでgituserのパスワードを指定します。

正常にプッシュされました。めでたしめでたし。

リポジトリからのクローン

別PCでリポジトリをクローンしてみます。
Visual Studioはクローンには対応していない(はず)なので、こればかりはコマンドラインから実行します。Visual StudioのプロジェクトのディレクトリからPowerShellを開き、
git clone ssh://gituser@[IPアドレス]/volume1/GitRemote/TestProject.git 
と入力すればリモートリポジトリからクローンします。
 
ちゃんとクローンされたプロジェクトが開けました。めでたしめでたし。

pushからの別PCからのpull

せっかく別PCでクローンしたので、こちらで編集してプッシュしてみます。
Hello, worldのコードを追加し、「チームエクスプローラー」→「変更」からローカルリポジトリにコミットします。
 
つづいて「チームエクスプローラー」→「同期」からプッシュします。

パスワードを入力したら無事プッシュされました。

元のPCに戻って、「チームエクスプローラー」→「同期」から今度は「プル」を選択します。

プルが完了したら見事にHello, worldのコードが出現しました。めでたしめでたし。

まとめ

これで無事NASにてGitサーバーを運用し、複数PCでプロジェクトを同期することができるようになりました。

Synology NASはadministratorグループに属していないとSSH接続ができないというちょっとアレな仕様があります。まあ、SSHでログインするなんてroot取って何かしらアレなことをしたい人くらいしかいないだろうという想定なんでしょうが、この仕様から、NASを利用する一般ユーザーにNAS上のリポジトリを触らせるのにはセキュリティ上の問題があります。これがどうにかならない限り、ソロGitからは抜け出せないでしょう。
えっ?ソロGitってpushする必要性あるの?????

あと、たぶんですけどVisual Studioは公開鍵認証をする方法が無さそうです。せっかくSSHならば公開鍵認証で認証を行って、毎回パスワードを入力しなくてもいいようにしたいんですけどねえ。