2020年9月14日月曜日

電子負荷装置を自作する

 さて、今回は(も?)ブログのタイトルに反してプログラミングは全く関係ない記事です。すみません。

1年ほど前、自作で電源装置を作りました。電源装置を作ったら当然負荷テストをしたくなりますよね。ということで、当時はセメント抵抗を何種類か買ってきてテストしていたのですが、抵抗値を変えようと思ったら繋ぎ変えが必要ですので、徐々に負荷を変化させていくということができません。思えば、この時から電子負荷装置が欲しかったのですが、ひとまずは電源装置も安定して動作しておりましたので特にその熱は一旦引いていました。

ですが、最近、日本橋の共立電子に行ったところ、 特設コーナーでスイッチング電源が安売りしていました。5V6Aが550円、12V9Aが1200円など、比較的高出力なものもかなり手ごろな値段で売っております(8月末~9月頭にかけての情報です。特売ですので、いつ行っても買えるとは限りません)。

普段は弱電ばっかり扱っている私ですので、たった数十Wでもパワみを感じて衝動買いしてしまいました。ですが、これだけ高出力な電源の性能を十分に発揮できるだけの負荷が手元にありません。

ここから、1年ほど前の「電子負荷装置」の熱が自分の中で再燃し、せっかくなので作ることを決意しました。

回路設計

電子負荷装置もいろいろなものがあります。商用レベルだと、数百Wとか数kWとかを消費でき、その上、その電力を電力系統に回生するようなものまで売っています。そういうものを自作するのは流石にレベルが高すぎますので、ここは簡単に数十W程度を消費できる単純な負荷を用意することとしました。

モードとしては、定電流モードと定抵抗モードを想定します。定電流モードは入力電圧にかかわらず電流を一定に保つモード、定抵抗モードは入力電圧に比例した電流を流すモードです。名前の通りですね。

この回路は、かなり簡単に作ることができます。オペアンプの基礎が分かっていれば、下記回路には何も説明はいらないでしょう。

たいてい回路図には現れませんが、オペアンプのLM358の電源は12V側から取っています。LM358は2回路入りなので、使わない側は周辺回路と容量結合とかして発振しないよう、適切に接地等してあげましょう。

SW1が定抵抗モード/定電流モードを切り替えるスイッチ、SW2がレンジのスイッチですね。

定電流モードで最大約10Aを流すことを想定しているので、オペアンプの+入力が最大約1Vになるよう抵抗値を調整しました。電源側ににさらに条件を付ければ外部電源無しでも作れますが、それは嫌だったのに加え、電源が必要な電圧/電流計を使用することにしたので外部電源を使うようにしました。

熱設計

電圧、電流、許容損失

さて、上記の通り回路は超シンプルで簡単です。電子負荷装置を作る上で最も難しく、コストもかさむのは熱処理です。

例えばサンケン電子製の2SK3711のデータシートを見ると、定格電圧が60V、定格電流が70Aです。

2SK3711のデータシートより引用)

これだけ見ると、12V10Aくらい余裕のよっちゃんに見えます。

ただ、電子負荷装置は、この12V10AすべてをこのFETで受け止めることになります。たいていのスイッチング用途は何か別の負荷がドレインにつながっていて、それをFETでON/OFFするだけなので、FET自体にかかる電圧は低く、FETそのもので消費する電力もそこまで大きくはなりません。しかし、今回の用途ではすべての電力をFETで受け止めることになります(大事なことなので2度言いました)。

ですが、許容損失は130Wと、12V10A=120Wの損失を発生させても大丈夫そうです。ただし、チャネル温度を150℃以下に抑えられるならね。

そう、一番厳しいのはここ、チャネル温度なのです。定格電流にも定格電圧にも達していなくても、チャネル温度が150℃を超えたらFETが焼け切れてしまうのです。

熱抵抗

熱抵抗というパラメーターがあります。単位はK/Wです。熱伝導率の逆数ですね。「何Wの熱を流すのに何K温度の差が付くか」という物理量です。電気抵抗も「何Aの電流を流すのに何Vの電位差が付くか」という物理量ですね。そのアナロジーで考えると簡単です。

例によって2SK3711のデータシートを見ると、過渡熱抵抗θj-cが定常状態で0.95℃/Wくらいです。過渡熱抵抗と言うとわかりにくいですが、下付き文字の「j-c」は「ジャンクション-ケース間」の意味と思われます。すなわち、チャネルで発生した熱をケース(=FETのパッケージ)に伝えるのにどれくらい温度差が付くか、ということですね。

0.95℃/Wということは、たとえば120Wの損失を作るとチャネルとケースに120×0.95=114℃の温度差が付きます。 超強い放熱器を付けて、ケースを30℃に抑えたとしても、チャネル温度は143℃となり絶対最大定格のぎりぎりとなります。

一方で、例えば東芝製TK70J20Dならば、チャネル-ケース間熱抵抗が最大0.305℃/Wです。先ほどと同じく120Wを消費したとしても、チャネルとケースは36.6℃しか差が付かないということになりますね。これならば少し弱い放熱器を付けて、ケースが110℃くらいまで上昇してしまっても絶対最大定格を超えることはありません。さすが、値段が高いだけありますね。放熱特性が高性能です。

このように、大きな電力を消費させるFETの場合、電流や電圧といった特性よりも何よりも、このようにチャネル温度が最も設計上のボトルネックとなります。

ケース選定

ヒートシンク付きケース

さて、放熱がどれほど重要か、という話をしましたが、そこで重要になってくるのが放熱器です。例えば共立電子ではジャンク扱いでクソデカ放熱器が売っていたりしますが、ちょっとこれはネタすぎてカッコよくないですよね。

今回はタカチ製のヒートシンクアルミケースのEXHシリーズを採用しました。パネルの大きさ、熱抵抗などからEXH14-5-19BBを選択しました。熱抵抗2.27℃/Wです。ちなみに黒アルマイト品のほうが値段が少々高くつきますが、放熱器は黒のほうが黒体放射で冷却性能がやや高いらしいです。ほんとかよ。

このケースだけで値段が5000円程度してしまいます。回路だけだと1000円も出せば十分に組み立てられてしまうのに、ほんと、「電子負荷装置の自作は放熱機構の自作である 」という感じですね。

なお、このケース、放熱器側にナット用の溝があり基板を取り付けることができます。表面にネジ穴を出さないスマート設計ですね。

カタログPDFより引用)

アルミ板を用意し、そこにFET、シャント抵抗、基板を固定しケースに取り付けました。良い感じです。

アルミ板と放熱器の間の空間

さて、この取り付け方、カタログPDFに書いてある通りなのですが、いかんせん、放熱器への熱が流れるルートが少なすぎます。アルミ板とケースが接しているのはナットのレール部分のみ、放熱器本体とアルミ板の間は空気です。空気はかなり熱抵抗が大きな素材です。これでは、効率よく熱を逃がせません。

そこで、間を熱伝導率の高いもので埋めることを考えました。調べていたところ、ワイドワーク製の放熱ゴムというものを見つけました。図面上は隙間が6mm程度ですので、12mm厚のものを購入しました。潰して半分くらいの厚さにできるかなと思いましたが、硬く無理だったので、ハサミでカットして使いました。

熱伝導率2.4W/m・Kなので、面積100mm×100mm(=0.01㎡)、厚さ6mm(=0.006m)とすると2.4×0.01/0.006=4W/Kになります。逆数を取れば熱抵抗で、0.25K/Wとなります。ケースの熱抵抗は2.27K/Wなので、足して2.5K/W程度となります。これで、そこそこの放熱環境を整えられたと言えるでしょう。

耐久テスト

自然冷却

さて、調子に乗って電流を流すと次々とFETが焼けていくんですねえ。さらに、チャネル温度が限界突破して焼けた場合、ドレイン―ソース間がショートモードで故障するらしく、あまり良くない状況になります。

最終的に、TK70J20Dを使って連続30W運転程度が落としどころとなりました。

チャネル―外気間の熱抵抗を3K/Wとすると、30Wで約90℃の上昇となります。気温25℃なら115℃ですね。理論値はこうですが、いろいろと挟んでいるものもあるはずですので、まあこの辺が現実的な解かと思います。

強制空冷

もっと冷やしたくなったら強制空冷ですね。ケースの上にサーキュレーターを置いて、ガンガンヒートシンクを冷やしてみました。

80Wまでなんとかもちました。チャネル温度と気温の温度差を125℃とすると熱抵抗1.56K/Wです。もうすでにこの時点でケースの自然冷却時の熱抵抗を下回っています。さすが空冷ですね。一説によると、熱抵抗が1桁以上変わるんだとか。

ちなみに、90Wにしたら焼けました(◞‸◟)

こうやってみると、TDPが100Wを超えるようなCPUも普通にありますが、その性能をフルに発揮するヒートシンクって実はなかなかに大変なものなんでしょうね。

まとめ

というわけで、電子負荷装置を自作しましたが、話の中心は電気的なところではなく熱的なところとなりました。まあそれも予定調和ですね。

前半のほうに少し触れた電力系統への回生機能を持った電子負荷装置は、単にエコだけでなく、このような強烈な放熱機構の必要性という面からも非常に重要なもの、ということですね。数百Wも放熱するような電子負荷装置を作るとしたら、一体どれだけ大きな放熱機構が必要になることやら。

 

おまけ

デカール貼り

今回、レンジとモードのスイッチを持っていて表記を作らないとわかりにくいことから、デカールを用意して貼り付けました。

小さいころから鉄道模型はやっていたのでインスタントレタリングこと「インレタ」は何度かやったことがあったのですが、デカールはたぶん初めてでした。

難しいですね。小さい文字なんか、器用に貼ろうとしてもどうしても傾いてブサイクになってしまいます。

殉職したFETの方々

今回、実際どの程度FETが持つのかということを知りたくて、FET焼く覚悟で高負荷をかけて実験しました。これらのFETの死は無駄ではなかった…と信じたいです。どうか、来世ではスイッチング電源にでもなって活躍されますように。

FET交換の工夫

最初はFETをはんだ付けしていたのですが、だんだんとそれ自体が面倒になってきます。だってすぐに焼けるんだもん。

ということで、端子台を用意し、はんだ付け不要でFETを交換できるようにしました。

2020年6月28日日曜日

System.Interactiveで快適LINQ生活

久しぶりにプログラム書いてたらライブラリの名前をすっかり忘れて探すのにだいぶ苦労したのでメモしておきます。

C#3.0で導入されたLINQも、少し痒いところに手が届かないことがあります。
例えばDistinctメソッドは基本的に要素本体でしか重複判定をすることができません。LINQならば、例えば以下のようなことしたいですよね。
var uniqueName = data.Distinct(p => p.Name);
何かのデータで同じ名前の重複を削除したいときはこう書きたいです。
実際には他のプロパティが違うデータでも同じ名前だったら1つを残して削除されちゃいますし、どれが削除されるのかは直感的にはわかりません。そういった不都合があってこのようなメソッドが作られなかったのかもしれませんが、でも実際問題使いたいことが結構あります。

他にも、例えばSelectManyメソッドは「配列の配列」をそのまま縦につなげて1つの配列にするのに便利ですが、その逆、すなわち配列をいくつかごとに分割して「配列の配列」にするメソッドはありません。

そんなもろもろの痒いところに手を届かせたライブラリは実はMicrosoft公式で存在します。


Nugetから入れれば、一発でDistinctメソッドにキー選択機能があるオーバーロードが追加されます。めでたしめでたし。ちなみに後者の「SelectManyの逆」はBufferメソッドです。

このライブラリはReactive Extensionsの一部です。まあ、LINQを配列のように「位置的に並んでいるもの」だけじゃなく「時系列的に並んでいるもの」にまで拡張させたのがReactive Extensionsですから、その流れでできた新しい便利な拡張メソッドをIEnumerable<T>用の拡張メソッドに逆輸入した形なのでしょう(たぶん)。

備忘録でブログに残しておいただけなので、今日はこれくらいで。

2020年6月24日水曜日

WPFでChromium埋め込みブラウザを使用する [CefSharp]

自分のソフトの中でブラウザを埋め込みたいことってありますよね。
WPFではWebBrowserコントロールが用意されていますが、とにかく使いにくい上に、IE7ベースで動作するという非常に残念なものです。IE11/Edgeに切り替えることもできなくはないようですが…レジストリをいじることになるのでアプリケーションとして使うというよりか、システムとして設定を変更することになってしまいます。
どうせならGoogle Chromeなどが代表的なChromiumブラウザ系統の組み込みブラウザを使いたいですよね。最近、IE系統に対応せずにChrome対応を謳うサイトも増えてきたことですし。まあ私はFirefox派なんですが。

そこで今回紹介するのがCefSharpというライブラリです。
Chromium Embedded Framework (CEF)というChromiumブラウザの表示部分のみを抜き出したのフレームワークがあり、これをC#からアクセスできるようになったものがCefSharpです。

導入

導入は至って簡単です。Nugetから入れることができます。
今回はWPFで使用するので、CefSharp.WPFを入れましょう。そうするとついでにCefSharp.Commonも入ります。
注意点ですが、Targetをx86かx64に指定してやる必要があります。Any CPUだと動きません。

使用

これも簡単です。CefSharp.Wpf.ChromiumWebBrowserをXAML上で定義してあげるだけです。
<Window x:Class="WebBrowserTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WebBrowserTest"
        xmlns:cef="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="1080" Width="1920">
    <Grid>
        <cef:ChromiumWebBrowser Address="https://www.google.co.jp" />        
    </Grid>
</Window>
めでたくブラウザが表示されます。

Navigate

任意のURLのWebページに遷移したいことがありますよね。
標準のWebBrowserコントロールは厄介で、Sourceプロパティが依存関係プロパティではありません。そのため、バインディングをすることができず、ビヘイビアを使うなどして操作しないといけません。超面倒でした。
CefSharpはそこのところはちゃんと考えてあり、Addressプロパティにバインディングすることができます。
<Window x:Class="WebBrowserTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WebBrowserTest"
        xmlns:cef="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="1080" Width="1920">
    <DockPanel>
        <TextBox DockPanel.Dock="Top" x:Name="Address" Text="https://www.google.co.jp"/>
        <cef:ChromiumWebBrowser Address="{Binding ElementName=Address, Path=Text, Mode=TwoWay}" />        
    </DockPanel>
</Window>
TextBoxのTextをTwoWayでバインディングすることで、いわゆるブラウザのアドレスバーのような形で使用することができます。

スクリプトを実行

せっかく自分のプログラム上でブラウザを動かしているのですから、何かしらブラウザ上でアクションを起こしたいですよね。
そんな人の為にExecuteScriptAsyncという拡張メソッドが用意されています。実行するにはChromiumWebBrowserのインスタンスが必要ですので、MVVMを維持しながら作るならばビヘイビア等を活用して作る必要がありますが、今回は簡単のために、ボタンを押したらコードビハインドでスクリプトを実行するプログラムを作ってみます。
<Window x:Class="WebBrowserTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WebBrowserTest"
        xmlns:cef="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="1080" Width="1920">
    <DockPanel>
        <TextBox DockPanel.Dock="Top" x:Name="Address" Text="http://orteil.dashnet.org/cookieclicker/"/>
        <Button DockPanel.Dock="Bottom" Content="Earn a million cookies" Click="Button_Click" />
        <cef:ChromiumWebBrowser x:Name="browser" Address="{Binding ElementName=Address, Path=Text, Mode=TwoWay}" />        
    </DockPanel>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        browser.ExecuteScriptAsync("Game.Earn(1000000)");
    }
}
こんなコードを用意すると、ボタンを押すだけでクッキーが100万個生産されます。
True NeverclickなんていうAchievementを解除してしまいました。

スクリプトを実行してその値をC#側で受け取りたいこともあると思います。
そういうときは、EvaluateScriptAsync拡張メソッドを使います。
private async void Button_Click(object sender, RoutedEventArgs e)
{
    var response = await browser.EvaluateScriptAsync("document.getElementById('cookies').innerText");

    if(response.Success && response.Result != null)
        MessageBox.Show(response.Result.ToString());
}
非同期メソッドであることに注意してください。
上のメソッドを実行すると、このように'cookies'というIDを持った要素のinnerTextプロパティの値を返してきます。
注意点として、返却値は配列と文字列はOKみたいですが、それ以外のオブジェクトはダメなようです。微妙に使いにくいですが…。


てなところです。
私自身も使い始めたばかりなので全然触れていない部分もありますが、まあ、少なくとも標準のWebBrowserコントロールよりかは使いやすそうです。
こういうの使って何とかC#のステージに引きずり下ろすより、とっととJavascriptをしっかり勉強してブラウザのアドオンとか作ったほうがいいのかなあ。

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#に触ったんだなあ。また余裕ができたら何か作りたいもの考えてプログラム書かないとな。