2014年6月27日金曜日

Fluent Ribbon Control Suiteの文字を大きくする。

さて、前回Fluent Ribbon Control Suiteを導入した話をしましたが、デフォルトのフォントが小さすぎるんですね…。こんな感じです。



ところで、どうもこのコントロールは文字サイズの変更を想定して設計されていないらしいです。なんてこった…。
一応FontSizeプロパティに値を書くことで部分的にフォントは変えられますが、全てのコントロールにそれを書き込んでいくのはナンセンスです。おまけに、フォントサイズを変えたところでタブの高さ等は変わらないので、文字が部分的に隠れて見えなくなったりしてしまいます。

そこで、LayoutTransformを使ってみることにしました。WPFの画面表示はGPUが司っているので、割と表示内容を行列変換的なことで変形しやすくなっています。
ScaleTransformで縦横それぞれ好きな比率で引き伸ばせますね。DockPanelを引き伸ばしてみましょう。

        <DockPanel.LayoutTransform>
            <ScaleTransform CenterX="0" CenterY="0" ScaleX="1.2" ScaleY="1.2" />
        </DockPanel.LayoutTransform>

はい、縦横それぞれ1.2倍ずつしています。
しかし、これではちょっとダサくなってしまいました。


コントロール全体は大きくなったものの、ウィンドウ枠の半透明な部分のサイズが変わっていないので、リボンタブを突っ切る形で半透明な部分が終わっています。これではとてもダサいです。
そこで、RibbonWindowのGlassBorderThicknessを指定してみました。
"8,60,8,8"をセットすることで、タブの位置に合わせて透明部分を下にずらすことができます。


はい。透明な部分は下がりましたが、その透過率を下げるグラデーション?みたいなやつの位置が変わらないのでこれもこれでとてもダサくなってしまいました。
いろいろいじってみましたけど、これは変わりそうにも無いですね…。
ならばいっそのこと、その半透明な部分を無くしてしまえばいいのだ!
というわけで、GlassBorderThicknessに"8,31,8,8"を入れてみました。


タブの部分の半透明な領域は無くなってしまいましたが、落としどころとしては悪くない気はします。

ただ、LayoutTransformで全体を1.2倍しているので、あらゆるパーツのフォントポイント等が大きくなってしまうんですよね…。その辺は若干気持ち悪いですが…。

なんかいい方法は無いんですかね。

2014年6月25日水曜日

リボンUI

個人的には、WinFormsではなくWPFを使うモチベーションとして、「リボンUIが使える」というものが挙げられます。

.NET4.0時代からMicrosoftはRibbonのライブラリを自社のページで公開していましたし、.NET4.5からはSystem.Windows.Controls.Ribbon名前空間内にRibbonクラスが組み込まれました。これにより、特に別途外部ライブラリを追加しなくてもリボンインターフェースを使ったアプリケーションが作れるようになりました。

そもそもWin32API時代から身からすると、さまざまなUIのコントロールはOS、もしくはそれに準ずるフレームワークが提供するのが常識でした(IEに新しいUIのコントロールが付いてきた時代を思い返せばとても懐かしいと思える人もいるかもしれません)。
そうやって多くのUIを共通パーツとしてDLLに入れてプログラマーに提供することで、アプリケーションからOSまで含めたシステム全体のサイズを小さくすることができ、ユーザーにとっても異なるアプリケーションで似たような一貫性のあるUIで操作できるというメリットが生まれます。
しかし、現代ではHDDやSSDの大容量化が進み、システム全体のサイズを気にするような時代でもありませんし、.NETなんかはかなりソフトウェアをパッケージングして提供する仕組みが(言語としてや、Nugetなどのサービスとしての両面から見て)とても優れています。
そのせいもあってか、Microsoftは最近あまりユーザーインターフェースの開発に積極的じゃないイメージがあります。基本的なボタンやコンボボックスなどのコントロールはさすがに提供してくれますが、WinFormsやWin32APIにあってWPFに無いコントロールもちらほら見かけます。さらに、IEやOfficeなどのMicrosoftのアプリケーションの中核商品に使われているコントロールも、昔はIEの新バージョンをインストールすれば使えるようになりましたが、今はあまり配布しているようには見えません。

しかし、リボンUIのライブラリはなぜかちゃんと提供してくれていたんですね。
やっぱり、当初VistaやOffice2007と同時に導入されてものすごく叩かれたからなんですかね。できるだけ多くのアプリケーションにリボンUIを使ってもらえれば、それだけリボンUIが世間に受け入れらていくものになるでしょう。


さて、それではリボンUIを導入してみましょう。

WPFのプロジェクトを作ります。私の場合はLivetのテンプレートから作ります。.NET4.5にすることを忘れないで下さい。


こんなかんじでウィンドウが出てきました。
次に、System.Windows.Controls.Ribbonへの参照を追加します。


つづいてソースコードを改造します。
と言っても非常に単純で、ウィンドウの親クラスがWindowからRibbonWindowになるだけです。
MainWindow.xamlとMainWindow.xaml.csの当該箇所を修正します。



あっれ~~~~~?????
なんかウィンドウ枠が小さくなってすごくダサくなっています。この現象が起こったのは.NET4.5からで、.NET4.0向けのライブラリでは問題ありませんでした。
まあそもそもリボンウィンドウの日本語記事があまり多くないのもあるのですが、これに関する日本語の情報は私が調べた限りでは皆無、英語の掲示板とかを見ても、結局有用な解決方法までは出てきませんでした。
例えばこのスレでは、最後にMicrosoftのWPF開発チームが書き込みをしていますが、もっと優先度が高い仕事はたくさんあるし、もしこれが本当にバグだと思うなら再現に必要な情報をもっと出せみたいなことを言っているようです。えー…再現できない環境とかあるのかよ…。

私が実際に条件を変えてコンパイルして実行してみた限りだと…
.NET4.0向けのライブラリ+.NET4.0=通常表示
.NET4.5内蔵ライブラリ=枠が細くなる
.NET4.0向けのライブラリ+.NET4.5=枠が細くなる
という再現結果になり、.NET4.5は使うなってことになってしまいました。
なんかこういうのは気に食わないですねえ。新しい機能が使えなくなってしまいます。
どちらかというと私はAppleよりMicrosoft信者ですが、こういうデザインに関する根本的な問題がでてきたら「Appleなら…」という気持ちも出てきてしまいます。

そのような中途方に暮れていると、こんなライブラリを発見しました。

Fluent Ribbon Control Suite

デザインがとても忠実にOffice2010のスタイルを再現しています。
これはもう公式ライブラリを捨ててこれを使うしか無いんじゃないか!?
ドキュメンテーションも結構しっかりと書いてあったりします。サンプルコードも充実しています。

というわけで導入してみました。
導入でつまずいたところは、最新のLivetのプロジェクトテンプレートだとなんかのファイルで競合を起こしてうまくビルドができない点ですかね。
仕方がないので、ただのWPFプロジェクトを立ち上げてから、Fluent RibbonとLivetの両方をNuget経由で入手しました。

Livetの流儀でウィンドウをViewsディレクトリ下に入れるの好きなので、デフォルトでついてくるMainWindow.xamlは削除し、Viewsディレクトリの中にMainWindow.xamlを入れます。そうすると、スタートアップウィンドウのパスが変わるので、App.xamlを修正してやります。
ついでに、 テーマのリソースを指定してやる必要があるので、それを追加します。

<Application x:Class="RibbonTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="Views\MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary Source="pack://application:,,,/Fluent;Component/Themes/Office2010/Silver.xaml" />
    </Application.Resources>
</Application>

あとは、標準のリボンライブラリとほとんど同じです。
 親クラスをWindowからFluent.RibbonWindowにしてやって、一通りのコントロールをXAMLで記述します。こんなかんじです。

<Fluent:RibbonWindow x:Class="RibbonTest.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:RibbonTest.Views"
        xmlns:vm="clr-namespace:RibbonTest.ViewModels"
        xmlns:Fluent="clr-namespace:Fluent;assembly=Fluent"
        Title="MainWindow" Width="640" Height="480">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
     <i:Interaction.Triggers>     
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    
    <DockPanel>
        <Fluent:Ribbon DockPanel.Dock="Top">
            <Fluent:Ribbon.Menu>
                <Fluent:Backstage Header="Menu" >
                    <Fluent:BackstageTabControl>
                        <Fluent:BackstageTabItem Header="New" />
                        <Fluent:BackstageTabItem Header="Open" />
                    </Fluent:BackstageTabControl>
                </Fluent:Backstage>
            </Fluent:Ribbon.Menu>
            <Fluent:RibbonTabItem Header="Home">
                <Fluent:RibbonGroupBox Header="Group1">
                    <Fluent:Button Header="Button1" />
                    <Fluent:Button Header="Button2" />
                </Fluent:RibbonGroupBox>
            </Fluent:RibbonTabItem>
            <Fluent:RibbonTabItem Header="Insert">
                <Fluent:RibbonGroupBox Header="Group2">
                    <Fluent:Button Header="Button3" />
                    <Fluent:Button Header="Button4" />
                </Fluent:RibbonGroupBox>
            </Fluent:RibbonTabItem>
        </Fluent:Ribbon>
        <TextBox />
    </DockPanel>
</Fluent:RibbonWindow>

これを起動するとこんな感じになります。


見事にそれっぽいリボンUIができました。
ついでに、Menuを押すとこのような画面に遷移します。


ウッヒョオオオオオォォォォォォ!!めっちゃOffice2010っぽい!!
Microsoftの標準ライブラリはOffice2007スタイルで、メニューボタンを押すとドロップダウンメニューが出てくるだけなんですよね。

ただ1つ、なんかフォントが小さいですね…。
FontSizeとかLayoutTransformとかでいじってみましたが、どれも満足行く結果は得られずじまいでした。
ただ、テーマという概念があるっぽいので、どうにかできないかちょっと検討してみます…。

ラングレーの問題ソルバー

ここんとこ組み込みの話ばかりしていましたが、一応プログラミングのブログということで、今度はパソコン上で動くアプリケーションのプログラミングの話をしていきます。

ラングレーの問題という問題があります。

ラングレーの問題 - Wikipedia

はい。中学だか高校だかで一度は見たことある図形ですね。
各々の角度を計算して書きこむだけでは答えが出ないので、何か補助線を引いたり、何かしらの図形の相似を証明するなどしながら解いていかなければいけないので、結構難問として図形を覚えてる人もいるかと思います。

このラングレーの問題、別にこの形のこの角度である必要はどこにもなく、底辺の1本の線分に対してそれぞれの頂点から2本線を生やして交点をいい感じに結べばそれはラングレーの問題になるっぽいです。特に、それぞれの角度を10°の倍数にしておいたらいい感じに求める角xも10°の倍数になる問題が100以上もできるそうです。すごいですねー。

昨日出会ったのはこの問題でした。


(a,b,c,d)=(10°,70°,60°,20°)です。解いてみるとさっぱりわかりません。
結局、このような神がかった補助線を引くことで答えが出るらしいです。


はい。点Eは△DBCの外心、点Fは△ABCの内心です。点EはAC上に乗り、点D,E,Fは一直線に並びます。そして点D,A,F,Cは同一円周上に乗るので、ゴニョゴニョ計算してやると∠ADBが求まるわけです。こんなの思いつかねえよ…。

ところで、このラングレーの問題、神がかった補助線を思いつくかどうかは別問題として、線分BCの両端から生える線と線分BCのなす角を固定してやれば、点A,B,C,Dの位置は確定するので、力技で角度を求めることができるようになります。
ということは、コンピューターで答えを計算したくなりますよね?なってください。

早速ソフトを書いてみました。

パソコン上で動作するアプリケーションを開発するときは、私はいつもC#+WPF+Livetで作っちゃいます。そして、たいてい便利なコントロールを導入するためにExtended WPF Toolkitを入れます。
Livetはプロジェクトテンプレートを簡単にインストールできますし、Extended WPF ToolkitはNugetから簡単にインストールできます。とても便利です。


そしてこのようなウィンドウができました。右のプロパティ設定欄のInput Angleに4つの角度を入力するだけで、角度xを計算して表示してくれます。

計算はとても簡単です。
まず、図を描画するためにも、点Aと点Dの座標を計算します。
線分AB、線分DBの長さが分かれば後は角度のサインやコサインを掛けてやれば座標は出るのでその線分を求めればいいのですが、この線分は正弦定理を使うとすぐに求まります。
座標がわかってて角度を求めたかったら、ベクトルの内積ですよね。DBベクトルとDAベクトルの内積を取って、DBベクトルの長さとDAベクトルの長さで割って、arccos取ったら∠ADBが出てきます。
やっているのはそれだけのことです。

PointB = new Point() { X = 0, Y = 0 };
PointC = new Point() { X = BaseLineLength, Y = 0 };

double diameter = BaseLineLength / Math.Sin(DegToRad(180 - (AngleA + AngleB + AngleC)));
double length = diameter * Math.Sin(DegToRad(AngleA + AngleB));
PointA = new Point() {
    X = PointC.X - length * Math.Cos(DegToRad(AngleC)),
    Y = PointC.Y - length * Math.Sin(DegToRad(AngleC))
};

diameter = BaseLineLength / Math.Sin(DegToRad(180 - (AngleB + AngleC + AngleD)));
length = diameter * Math.Sin(DegToRad(AngleC + AngleD));
PointD = new Point() {
    X = PointB.X + length * Math.Cos(DegToRad(AngleB)),
    Y = PointB.Y - length * Math.Sin(DegToRad(AngleB))
};

const double margin = 20;

Point LeftTop = new Point() {
    X = Math.Min(Math.Min(PointA.X, PointB.X), Math.Min(PointC.X, PointD.X)) - margin,
    Y = Math.Min(Math.Min(PointA.Y, PointB.Y), Math.Min(PointC.Y, PointD.Y)) - margin,
};
Point RightBottom = new Point() {
    X = Math.Max(Math.Max(PointA.X, PointB.X), Math.Max(PointC.X, PointD.X)) + margin,
    Y = Math.Max(Math.Max(PointA.Y, PointB.Y), Math.Max(PointC.Y, PointD.Y)) + margin,
};

Vector VectorDB = Vector.FromPointDifference(PointD, PointB);
Vector VectorDA = Vector.FromPointDifference(PointD, PointA);

AnswerAngle = RadToDeg(Math.Acos(VectorDB.DotProduct(VectorDA) / VectorDA.Length / VectorDB.Length));

計算はこんなかんじです。
WPFはY軸が下へ行くと大きくなるので、その点だけ注意してください。
PointクラスやVectorクラスも同時に実装しちゃいましたが、そんな大した実装じゃないですし、C#なのでこのコードを見れば何をやっているかはわかると思います。

<Line Stroke="{Binding LineBrush}" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointA.X}" Y1="{Binding PointA.Y}"
  X2="{Binding PointB.X}" Y2="{Binding PointB.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointB.X}" Y1="{Binding PointB.Y}"
  X2="{Binding PointC.X}" Y2="{Binding PointC.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointC.X}" Y1="{Binding PointC.Y}"
  X2="{Binding PointD.X}" Y2="{Binding PointD.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointD.X}" Y1="{Binding PointD.Y}"
  X2="{Binding PointA.X}" Y2="{Binding PointA.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointB.X}" Y1="{Binding PointB.Y}"
  X2="{Binding PointD.X}" Y2="{Binding PointD.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointC.X}" Y1="{Binding PointC.Y}"
  X2="{Binding PointA.X}" Y2="{Binding PointA.Y}" />

描画のほうはもちろんXAMLに記述しています。
Polylineでやったほうがスマートだったかもしれませんが、なんていうか、この図形は一筆書きできないので、そういう観点からしたらPolyline使うのもなーって感じだったので、まあとりあえずLineにしておきました。

座標や角度の計算とWPFの描画に関わることはこのへんですかね。

まあ、他にもPropertyGridを使うためのプログラムや、LayoutTransformを使った図形の拡大縮小、各頂点や角の名前を表示するためのプログラム等々書きましたが、ラングレーの問題とその図形の本質ではあまりないので、今回は省略します。
またなにか機会があったら記事にしてみようかなって思います。はい。

こういうちょっとしたプログラムって大したこと無いんで、ソースコードからソフトまで丸々公開しちゃってもいいんですけど、他人が作ったライブラリ使ってるといろいろと面倒なんですよね…。ライセンスを熟読しなきゃいけませんし、どんなにゆるいライブラリでも、「アプリ内のどこかにこのソフトウェアのThanksみたいなのを書いてね」っていうものが結構あったりしますし、今回使っているExtended WPF Toolkitはまさにそれです。
つまり、公開するためにはわざわざAboutダイアログを作らなきゃいけないんですね…。そこまで熱意を持って作ってるソフトでもなしにそこまでやるのはなかなか大変だったりします…。はい…。

余談


上の方の図で補助線を引いたものも用意しましたが、これもせっかくなんでWPFで全部補助線を描いてみました。
直線の補助線は、まあ計算しやすい座標を適当に計算して結べばいいだけですが、円はなかなかめんどくさかったですね。
円の方程式を一般形で記述します。\[x^2+y^2+lx+mx+n=0\]これに、円が通る点のうち3点の座標を代入してやればl,m,nが求まるわけですが、それはすなわち3元1次方程式なので、コンピューターで計算するには行列を計算させるのがスマートですね。\[\begin{pmatrix} x_1 &y_1 &1 \\ x_2 &y_2 &1\\ x_3 &y_3 &1 \end{pmatrix}\begin{pmatrix} l \\m \\n \end{pmatrix}=\begin{pmatrix} -x_1^2-y_1^2 \\-x_2^2-y_2^2 \\-x_3^2-y_3^2 \end{pmatrix}\]この行列に関して左側から逆行列をぶつけてやれば一発でl,m,nが求まります。しかし、どうもC#は算術計算のための行列ライブラリっていうのは標準で提供してないっぽいですね…。算術計算のための座標、ベクトルライブラリみたいなのも無くて今回は適当に実装しましたし、その辺は改善してもらいたいところです。
はい。3x3の逆行列を求めるプログラムまで実装していては気が遠くなってしまいます。掃き出し法だの何だのあったと思いますが、やってられません。というわけで、 このあたりのライブラリを使用させてもらいました。

C# .NET Generic Matrix Maths Library

というわけで、補助線部分のプログラムはこんなかんじになっています。

PointE = new Point() {
    X = PointC.X - BaseLineLength * Math.Cos(DegToRad(AngleC)),
    Y = PointC.Y - BaseLineLength * Math.Sin(DegToRad(AngleC)),
};
Point2E = PointE - PointD + PointE;

diameter = BaseLineLength / Math.Sin(DegToRad(180 - (AngleC / 2 + 40)));
length = diameter * Math.Sin(DegToRad(40));
PointF = new Point() {
    X = PointC.X - length * Math.Cos(DegToRad(AngleC / 2)),
    Y = PointC.Y - length * Math.Sin(DegToRad(AngleC / 2)),
};

DoubleMatrix m1 = new double[,] {
    { PointD.X, PointD.Y, 1 },
    { PointA.X, PointA.Y, 1 },
    { PointC.X, PointC.Y, 1 },
};
DoubleMatrix m2 = new double[,] {
    { -PointD.X * PointD.X - PointD.Y * PointD.Y },
    { -PointA.X * PointA.X - PointA.Y * PointA.Y },
    { -PointC.X * PointC.X - PointC.Y * PointC.Y },
};

DoubleMatrix res = m1.Inverse * m2;

double Radius = Math.Sqrt(res[0, 0] * res[0, 0] / 4 + res[0, 1] * res[0, 1] / 4 - res[0, 2]);
CircleDiameter = Radius * 2;
CircleLeft = -res[0, 0] / 2 - Radius;
CircleTop = -res[0, 1] / 2 - Radius;

doubleの2次元配列をそのまま行列に変換できて便利なライブラリですね。
これで円の中心の座標と半径が求まりますが、WPFで楕円を描画するのに必要なのは幅、高さ、左上の座標なので、直径と左上の座標を計算しています。

<Line Stroke="{Binding LineBrush}" StrokeDashArray="1 2" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointD.X}" Y1="{Binding PointD.Y}"
  X2="{Binding Point2E.X}" Y2="{Binding Point2E.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeDashArray="1 2" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointE.X}" Y1="{Binding PointE.Y}"
  X2="{Binding PointB.X}" Y2="{Binding PointB.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeDashArray="1 2" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointC.X}" Y1="{Binding PointC.Y}"
  X2="{Binding PointF.X}" Y2="{Binding PointF.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeDashArray="1 2" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointB.X}" Y1="{Binding PointB.Y}"
  X2="{Binding PointF.X}" Y2="{Binding PointF.Y}" />
<Line Stroke="{Binding LineBrush}" StrokeDashArray="1 2" StrokeThickness="{Binding LineThickness}" StrokeEndLineCap="Round"
  X1="{Binding PointA.X}" Y1="{Binding PointA.Y}"
  X2="{Binding PointF.X}" Y2="{Binding PointF.Y}" />
<Ellipse Stroke="{Binding LineBrush}" StrokeDashArray="1 2" StrokeThickness="{Binding LineThickness}"
         Canvas.Left="{Binding CircleLeft}" Canvas.Top="{Binding CircleTop}" Width="{Binding CircleDiameter}" Height="{Binding CircleDiameter}" />

補助線は点線にするために、StrokeDashArrayというプロパティに値を入れています。
WPFはこんなふうにかなり柔軟に点線が引けるんですね。驚きました。

これでめでたく補助線が引けました。

あ、これ、問題の角度が変わってくると解き方も全くもって変わるので、この補助線が使えなくなることはお間違え無きよう。

2014年6月21日土曜日

XC16でコンパイル最適化を掛ける

さて、ここ暫くの間NTP時計を家で運用していますが、特に大きなバグ等にはぶち当たっていません。めでたく安定動作しております。

ところで、コンパイラにはコンパイル最適化というものがあります。C言語のコードは基本的に人間様への可読性が命です。アルゴリズムとかそういう根本的なところではスピードやコードのコンパクトさを意識する必要はあるかもしれませんが、細かな記法において、わざわざ人間様への可読性を大幅に下げてまでスピードを上げるという考え方には懐疑の念を抱かざるを得ません。もちろん、熟練したプログラマーとして可読性を維持したまま高速・コンパクトな記法をできるのならばそれに越したことはありませんが。

はい、話がずれてきましたが、要は、コンパイラには「人間様が多少非効率なコードを書いたとしても、それをコンパイル時に効率的なコードに変換する機能」が求められます。それが、コンパイル最適化です。

しかし、コンパイル最適化には、特にPICのような組み込みマイコンではしばしば問題が起きます。

例えば、EEPROMアクセス。PIC16シリーズとかPIC18シリーズでは、EEPROMに書き込むときに、書き込みシーケンスとしてEECON2に0x55を書き込んだあとに0xAAを書き込み、そしてEEPROMにデータを書き込むという動作をしなければなりません。これは、偶発的なトラブルで不用意にEEPROMが書き換えられてしまわないように冗長性を持たせるという意味が込めてあります。
このようなクリティカルなコードを実行する場合、C言語の開発であってもインラインアセンブリで書いてしまうことも多いとは思いますが、 仮にC言語で書いたとしましょう。

EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;

はい。こんな感じになるかと思います。
これはおそらく最適化を掛けなければ正常に動作するでしょうが、最適化を掛けた場合、コンパイラが「EECON2に0x55を書き込んだ直後に0xAAを書き込んでるし、その間では参照されていないから0x55を書き込む必要なんて無いね」と判断しかねません。もっと言えば、この先でEECON2を読み出すこともおそらく無いでしょうから、0xAAを書き込むコードすらコンパイル時に消されてしまうかもしれません。

これは、ときに非常に難解な問題になります。
どう見てもソースコードには書き込みシーケンスが書いてありますが、コンパイルして出来上がったオブジェクトファイル及びHEXファイルにはこのシーケンスは入っていません。しかし、一般にオブジェクトファイルやHEXファイルは人間様が読めないので、 その問題に気づけることは知らないとなかなか無いでしょう。そして「よくわからないけど動かない」という現象が起こるわけです。

はい。このような問題はしばしば起こります。なので、最適化においてはこのようなことを念頭において置かなければいけません。

では、上記のようなコードを書きたいときはどうするか?
はい。「volatile」という修飾子がC言語には用意されています。 英語の意味は「揮発性の」とか「変わりやすい」とかそういう意味だそうです。これを変数宣言時に付けると、その変数に関わる最適化を抑制することができます。なので、おそらく組み込みマイコンのSFRのアクセスに関してなんかは、ライブラリの中でその変数にvolatileが付けられた状態でtypedefなり何なりがされていることでしょう。


はい、要するに何が言いたいかと言いますと、最適化は「賭け」なんです。
コードの実行速度やコードサイズは小さくなるかもしれませんが、自分が意図しないところまで最適化されてしまうとソフトウェアが上手く動かなくなる可能性があります。特に、単にロジックをコードで表現しただけのようなプログラミングと違ってハードウェアを制御する組み込みプログラミングでは最適化でしばしば起こりそうな問題があるからです。
もちろん、コンパイラの最適化の仕様を熟知していて、さらに他人が作ったライブラリも含めて最適化を掛けられる前提でvolatileなどの修飾子を適切に付けてあるコードだったら問題は無いのでしょうが、そこまでやるのはなかなか難しいものです。

でもまあとりあえずやってみました。上手く動かなかったら最適化をやめてプログラムを書き直せばいいかなって。


最適化をやるのは簡単です。プロジェクトのプロパティからxc16-gccを選び、option categoriesをOptimizationsにします。そして、Optimization levelを設定すればいいだけです。細かな説明はOption Descriptionのタブに書いてくれています。
上位の最適化は有料版XC16などでしか有効にならないので、とりあえず無料版でできる最適化レベル1にしてコンパイルしてみましょう。


こちらが最適化レベル0(最適化なし)のときのコードサイズです。プログラムメモリは85%も使っています。


そして、最適化レベルを1にしたらこうなりました。見事に66%までコードサイズが減っています。

さて、問題の最適化の副作用ですが…今のところ特に無さそうですね。
まあ、様子見です。

2014年6月18日水曜日

グラフィック液晶の表示制御

さて、今回は少し戻ってまたNTP時計にまつわる話をしたいと思います。
今回は、C言語によるソフトウェアの設計技法のような話になります。

以前の記事でソフトウェアSPIを実装し、フォントを2種類実装したという話を書きました。しかし、それだけでは不十分で、このあたりはスイッチに合わせて表示内容をヒョコヒョコ切り替えなければいけませんね。例えば、7セグには表示されていない日付を表示するモード、もしくはIPアドレスやMACアドレスなどのネットワークの状態を表示するモード、CdSから取得した環境の明るさを表示したり7セグの明るさを変える閾値やその明るさを設定するモード、NTPサーバーを設定したりNTPサーバーとの同期間隔を設定するモードなどが必要になってきます。
表示モードを切り替えるくらいならばスイッチに合わせて表示内容を変えていけばいいのでしょうが、時には設定を編集したりするなどの複雑な処理をする場合は、スイッチが押された時の制御が結構複雑になってしまい、難解なソースコードになってしまいます。

難解なソースコードでは得する人はだれもいません。なので、できるだけわかりやすくプログラムを書かなければなりません。

こういうソフトウェアの設計に慣れた人だと、当然、スイッチが押されたときに呼ばれる関数は表示モードごとにあるべきで(=各モードの制御の中にスイッチの処理が入る)、例えばBlueButtonDown()関数などをシステムで1個用意し、そこから現在の表示モードによってswitch文でて処理内容を変える(=スイッチの制御の中に各モードの制御が入る)ような処理はスマートとは言えません。
もちろん、実装としては後者のほうが果てしなく楽というか、C言語の組み込みプログラミングでは自然なのですがね。スイッチが押されること自体はモードにかかわりなく発生するイベントですし、そもそも「処理をまとめる」という文化がほとんど無い言語ですから。

例えばこれがC言語ではなく、C++やC#などのオブジェクト指向言語ならばもうやることはとても楽になります。
抽象クラスでBlueButtonDown()やBlueButtonUp()、DrawDisplay()などの関数を定義し、デフォルトの処理を実装しておきます。そして、各表示モードではその抽象クラスを継承し、必要に応じてそれらの関数をオーバーライドします。システムにはその各表示モードのクラスのインスタンスを渡し、システムはそのインスタンスを配列として持っておけば、モードが切り替わるたびにその配列のどこを表示しているかを順々に変えていけば良いだけです。とてもスマートな実装になります。

しかし、C言語はこのような高度なオブジェクト指向プログラミングはできないんですよね。C++ならできます。しかし、XCコンパイラは8bit、16bitアーキテクチャ向けはC言語しかサポートしておらず、C++をやりたければ32bitマイコンのXC32++くらいになってしまいます。当然、NTP時計はPIC24FですのでC++は使えないことになってしまいます。

というわけで、できるだけC言語でこのオブジェクト指向に近い書き方をすることにしてみましょう。

typedef void (*DrawLCDCallback)(BOOL);
typedef void (*SwitchCallback)(void);

typedef struct _tagLCDPAGE {
    int SettingState;
    DrawLCDCallback DrawLCD;
    SwitchCallback RedOn;
    SwitchCallback RedOff;
    SwitchCallback BlueOn;
    SwitchCallback BlueOff;
    SwitchCallback YellowOn;
    SwitchCallback YellowOff;
} LCDPAGE, *PLCDPAGE;

まずはこのような構造体を定義します。
はい、まさに「関数ポインタ」というやつです。詳しくは割愛しますが、関数だってメモリ上のどこかにあるものだから、それを抽象化してポインタとして指せるような機能があってもいいよねってやつです。はい。ここで、LCDのページ(いわゆる各モードの表示内容とボタンが押された時の処理)の元となるいわゆる抽象クラスのようなものとしています。

SettingStateは設定のときの状態です。多くはカーソルとして使っていて、ある数が例えばある桁の編集に対応しています。アクティブではないページではこの値がゼロにリセットするようにしています。

あとはLCDの描画時に呼ばれる関数、各ボタンが押された時と離された時に呼ばれる関数を用意しています。 LCDの描画は、効率のために再描画する必要があるかどうかのフラグをシステムから渡すようにしています。例えばページが切り替わった直後は再描画する必要があるのでTRUEをシステムが渡してきます。しかし、それ以外の時は、ページが必要と思ったところのみを更新すればいいのでFALSEを渡してきます。

例えば、ホーム画面(今日の日付と直前にNTPサーバーと同期した日時の表示)のプログラムHome.cはこんな感じになっています。

#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include "AQM1248.h"
#include "PageBase.h"
#include "SNTPx.h"


void Home_Draw(BOOL bForceRedraw)
{
    char text[18];
    static char LastDate[18] = {0};
    QWORD utcms;
    static QWORD LastUpdateMsec;
    struct tm Time;

    //Show Date
    utcms = SNTPGetUTCMilliseconds();
    UTCMillisecondToLocaltime(utcms, 9 * 60, &Time);
    strftime(text, sizeof(text) / sizeof(char), "%Y/%m/%d (%a)", &Time);
    if(bForceRedraw || strcmp(text, LastDate)) {
        LCD_FillPattern(0xFF, 0, 0, LCD_COLUMN_MAX, 1);
        LCD_PrintText("Today's date", 0, 0, LCDFONT_SMALL | LCDFONT_REVERSE);

        LCD_PrintTextAtCenter(text, 1, LCDFONT_MEDIUM);
        strcpy(LastDate, text);
    }

    //Show Last Sync Time
    utcms = SNTPGetLastUpdateUTCMilliseconds();
    if(bForceRedraw || (LastUpdateMsec != utcms)) {
        LCD_FillPattern(0xFF, 0, 3, LCD_COLUMN_MAX, 1);
        LCD_PrintText("Synchronized at", 0, 3, LCDFONT_SMALL | LCDFONT_REVERSE);

        UTCMillisecondToLocaltime(utcms, 9 * 60, &Time);
        strftime(text, sizeof(text) / sizeof(char), "%m/%d %H:%M:%S", &Time);
        LCD_PrintTextAtCenter(text, 4, LCDFONT_MEDIUM);
        LastUpdateMsec = utcms;
    }
}

PLCDPAGE LCDPage_GetHome()
{
    static LCDPAGE page;
    static BOOL bInitialized = FALSE;

    if(bInitialized == FALSE) {
        page.SettingState = 0;
        page.DrawLCD = Home_Draw;
        page.RedOn = NULL;
        page.RedOff = NULL;
        page.BlueOn = NULL;
        page.BlueOff = NULL;
        page.YellowOn = NULL;
        page.YellowOff = NULL;

        bInitialized  = TRUE;
    }
    return &page;
}

ページのインスタンスはstaticインスタンスとして、初回にLCDPage_GetHome()を呼ばれた時に中身を初期化して返します。それ以降は初期化はしません。
main関数内ではLCDPAGE構造体の配列を持っており、こんな感じで初期化しています。

    //LCD Page
    pPages[0] = LCDPage_GetHome();
    pPages[1] = LCDPage_GetNetworkStatus();
    pPages[2] = LCDPage_GetNTPSetting();
    pPages[3] = LCDPage_GetNTPServerEdit();
    pPages[4] = LCDPage_GetOscillator();
    pPages[5] = LCDPage_GetCdSSettings();

至って簡単です。もしもページをこの後増やそうと思っても、配列の長さを増やしてここに追加してあげるだけでおkです。

void ProcessLCD()
{
    static uint8_t LastPageIndex;

    if(pPages[PageIndex]->DrawLCD != NULL)
        pPages[PageIndex]->DrawLCD(LastPageIndex != PageIndex);

    LastPageIndex = PageIndex;
}

main関数の無限ループの中で呼び出されるLCDの制御をする関数です。
こちらも至ってシンプルで、現在のページに対してLCD描画関数のDrawLCDがNULLじゃなければ呼び出します。呼び出すときは、以前のページと異なっていたら再描画の指示を出しています。

typedef struct _tagSWITCH_STATE {
    union {
        BYTE SwitchData;
        struct __PACKED {
            unsigned RED:1;
            unsigned BLUE:1;
            unsigned GREEN:1;
            unsigned YELLOW:1;
        } Switch;
    };
    QWORD UTCMillisecond;
} SWITCH_STATE;

void ProcessSwitch()
{
    static SWITCH_STATE ss[3];

    QWORD now = SNTPGetUTCMilliseconds();

    if(ss[0].UTCMillisecond != now) {
        SWITCH_STATE turn;

        ss[2] = ss[1];
        ss[1] = ss[0];
        ss[0].Switch.RED = SW_RED;
        ss[0].Switch.BLUE = SW_BLUE;
        ss[0].Switch.GREEN = SW_GREEN;
        ss[0].Switch.YELLOW = SW_YELLOW;
        ss[0].UTCMillisecond = now;

        turn.SwitchData = ~(ss[2].SwitchData) & ss[1].SwitchData & ss[0].SwitchData;
        if(turn.Switch.GREEN)
            Switch_GreenOn();    //OS Switch
        if(turn.Switch.RED && (pPages[PageIndex]->RedOn != NULL))
            pPages[PageIndex]->RedOn();
        if(turn.Switch.BLUE && (pPages[PageIndex]->BlueOn != NULL))
            pPages[PageIndex]->BlueOn();
        if(turn.Switch.YELLOW && (pPages[PageIndex]->YellowOn != NULL))
            pPages[PageIndex]->YellowOn();

        turn.SwitchData = ss[2].SwitchData & ~(ss[1].SwitchData) & ~(ss[0].SwitchData);
        if(turn.Switch.GREEN)
            Switch_GreenOff();    //OS Switch
        if(turn.Switch.RED && (pPages[PageIndex]->RedOff != NULL))
            pPages[PageIndex]->RedOff();
        if(turn.Switch.BLUE && (pPages[PageIndex]->BlueOff != NULL))
            pPages[PageIndex]->BlueOff();
        if(turn.Switch.YELLOW && (pPages[PageIndex]->YellowOff != NULL))
            pPages[PageIndex]->YellowOff();
    }
}

スイッチの処理は若干トリッキーなことをやっています。
今回のNTP時計にはチャタリング防止回路は入っていないので(ハードで入れるとハードが大きくなるという問題と、コンデンサを使うから瞬間的な大電流がスイッチに流れてスイッチが痛むという問題がある)ソフトウェアでそれをやる必要があります。1ミリ秒ごとにスイッチの状態をスキャンして、その3回分のデータがOFF→ON→ONとなっていたらスイッチがONされたとし、ON→OFF→OFFとなっていたらOFFになったと認識します。
あとは、まあ共用体と論理演算がわかってる人ならソースコード読めばだいたいどうなっているかがわかると思います。

緑スイッチだけはシステムで処理します。 つまり、各ページにはわたしません。緑スイッチが押されたらページを切り替えるので、その処理は絶対にページにオーバーライドはさせません。
それ以外のスイッチは、そのスイッチが変化して、かつそのページの処理関数のポインタがNULLでなければ呼び出すという非常にシンプルなものです。なので、例えばスイッチを使わないページがあったらそのポインタをNULLにしておけばいい、たったそれだけです。

C言語にはクラスはありませんが、一応1ファイル1クラスみたいな扱いとしてやることで、このようにそれなりにスッキリした実装ができました。
めでたしめでたし。

こういう実装を考えて完成させると達成感があって楽しいですが、やっぱりPICでC#動くと嬉しいな…。

USBメモリーをFatFsで操作する

さて、前回の時点でUSBメモリーをPIC32MX250F128Bからアクセスすることに成功しました。
FATファイルシステムもMicrochipのライブラリで制御しておりますが、やはりFATファイルシステムのライブラリと言ったらFatFsですね。(少なくとも私の中では)

FatFs 汎用FATファイルシステム・モジュール

もうね、このライブラリには何も不満は抱けないです。それくらい完成度の高いライブラリです。
ディスクの初期化や指定セクタの読み書きなどのほんのいくつかのデバイスに依存した関数をユーザーが用意するだけで、FAT16/32のアクセスができるようになります。
そのほか、非常に細かくオプションを指定できたりし(メモリが足りればLFNやUnicodeファイル名も使える)、その上、パフォーマンスもかなり良いです。言うこと無しです。

それでは導入していきましょう。

まずは上記のサイトよりFatFsのソースコードを入手し、ヘッダファイル、Cファイルをともにプロジェクトに登録します。ついでに、MicrochipのライブラリのほうのファイルFSIO.cを無効化しておきましょう。


次に、整数型の定義をします。integer.h内で、各ビット数に対応した型を定義してやるのですが、すでにPICのプロジェクトはGenericTypeDefs.hで同名の型が定義されており、二重定義エラーが起きてしまいます。しかし、BYTEだのWORDだの、この辺の定義はもはやビット数はだいたい決まっていて、FatFs内の定義とGenericTypeDefs.h内の定義が同じなので、integer.hでGenericTypeDefs.hをIncludeしてあげるだけにしました。WCHARの定義を残して、それ以外はコメントアウトします。

そしたら、一番重要なdiskio.cの定義をしてあげます。こんなかんじになります。

#include "diskio.h"
#include "ffconf.h"        /* FatFs lower layer API */

/* Definitions of physical drive number for each media */
#define USB        0


typedef struct
{
    BYTE    errorCode;
    union
    {
        BYTE    value;
        struct
        {
            BYTE    sectorSize  : 1;
            BYTE    maxLUN      : 1;
        }   bits;
    } validityFlags;

    WORD    sectorSize;
    BYTE    maxLUN;
} MEDIA_INFORMATION;

typedef enum
{
    MEDIA_NO_ERROR,                     // No errors
    MEDIA_DEVICE_NOT_PRESENT,           // The requested device is not present
    MEDIA_CANNOT_INITIALIZE             // Cannot initialize media
} MEDIA_ERRORS;

MEDIA_INFORMATION * USBHostMSDSCSIMediaInitialize( void );
BYTE USBHostMSDSCSIMediaDetect( void );
BYTE USBHostMSDSCSIWriteProtectState( void );
BYTE USBHostMSDSCSISectorRead( DWORD sectorAddress, BYTE *dataBuffer );
BYTE USBHostMSDSCSISectorWrite( DWORD sectorAddress, BYTE *dataBuffer, BYTE allowWriteToZero );


static BOOL isInitialized = FALSE;

/*-----------------------------------------------------------------------*/
/* Inidialize a Drive
*/
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize (
    BYTE pdrv                /* Physical drive nmuber (0..) */
)
{
    DSTATUS stat;

    switch (pdrv) {
    case USB:
        if(USBHostMSDSCSIMediaDetect()) {    //メディアがつながっているか?
            if(!isInitialized) {
                MEDIA_INFORMATION *mediaInformation;
                
                mediaInformation = USBHostMSDSCSIMediaInitialize();

                if(mediaInformation->errorCode != MEDIA_NO_ERROR)
                    stat = STA_NOINIT;
                else {
                    stat = 0;
                    isInitialized = TRUE;
                }
            } else
                stat = 0;

            if(USBHostMSDSCSIWriteProtectState())
                stat |= STA_PROTECT;
        } else
            stat = STA_NODISK | STA_NOINIT;

        return stat;
    }
    return STA_NOINIT;
}


/*-----------------------------------------------------------------------*/
/* Get Disk Status
*/
/*-----------------------------------------------------------------------*/

DSTATUS disk_status (
    BYTE pdrv        /* Physical drive nmuber (0..) */
)
{
    DSTATUS stat;

    switch (pdrv) {
    case USB :
        if(USBHostMSDSCSIMediaDetect()) {
            if(isInitialized)
                return 0;
            else
                return STA_NOINIT;
        } else
            return STA_NODISK | STA_NOINIT;

        return stat;
    }
    return STA_NOINIT;
}


/*-----------------------------------------------------------------------*/
/* Read Sector(s)
*/
/*-----------------------------------------------------------------------*/

DRESULT disk_read (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE *buff,        /* Data buffer to store read data */
    DWORD sector,    /* Sector address (LBA) */
    UINT count        /* Number of sectors to read (1..128) */
)
{
    UINT i;
    WORD SectorSize;

    if(disk_ioctl(pdrv, GET_SECTOR_SIZE, SectorSize) != RES_OK)
        return RES_ERROR;

    switch (pdrv) {
    case USB:
        for(i = 0; i < count; i++) {
            if(USBHostMSDSCSISectorRead(sector + i, buff + i * SectorSize) == FALSE)
                return RES_ERROR;
        }

        return RES_OK;
    }
    return RES_PARERR;
}



/*-----------------------------------------------------------------------*/
/* Write Sector(s)
*/
/*-----------------------------------------------------------------------*/

#if _USE_WRITE
DRESULT disk_write (
    BYTE pdrv,            /* Physical drive nmuber (0..) */
    const BYTE *buff,    /* Data to be written */
    DWORD sector,        /* Sector address (LBA) */
    UINT count            /* Number of sectors to write (1..128) */
)
{
    UINT i;
    WORD SectorSize;

    if(disk_ioctl(pdrv, GET_SECTOR_SIZE, SectorSize) != RES_OK)
        return RES_ERROR;

    switch (pdrv) {
    case USB :
        for(i = 0; i < count; i++) {
            if(USBHostMSDSCSISectorWrite(sector, (BYTE *)buff + i * SectorSize, TRUE) == FALSE)
                return RES_ERROR;
        }

        return RES_OK;
    }
    return RES_PARERR;
}
#endif


/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions
*/
/*-----------------------------------------------------------------------*/

#if _USE_IOCTL
DRESULT disk_ioctl (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE cmd,        /* Control code */
    void *buff        /* Buffer to send/receive control data */
)
{
    DRESULT res;
    int result;

    switch (pdrv) {
    case USB :
        switch(cmd) {
            case CTRL_SYNC:
                res = RES_OK;
                break;
            case GET_SECTOR_COUNT:    //フォーマットするときにしか使われない
                res = RES_ERROR;
                break;
            case GET_SECTOR_SIZE:
#if(_MAX_SS == _MIN_SS)
                *((WORD *)buff) = _MAX_SS;
                res = RES_OK;
#else
                res = RES_ERROR;
#endif
                break;
            case GET_BLOCK_SIZE:
                *((DWORD *)buff) = 1;
                res = RES_OK;
                break;
            case CTRL_ERASE_SECTOR:
                res = RES_OK;
                break;
            default:
                res = RES_PARERR;
                break;
        }
        return res;
    }
    return RES_PARERR;
}
#endif

typedef union _tagFATTIME {
    DWORD value;
    struct {
        unsigned SecDiv2 : 5;
        unsigned Min : 6;
        unsigned Hour : 5;
        unsigned Date : 5;
        unsigned Month : 4;
        unsigned YearFrom1980 : 7;
    };
} FATTIME;

DWORD get_fattime (void)
{
    FATTIME time;

    time.YearFrom1980 = 34;
    time.Month = 6;
    time.Date = 17;
    time.Hour = 23;
    time.Min = 16;
    time.SecDiv2 = 0;

    return time.value;
}

void disk_detatched(void)
{
    isInitialized = FALSE;
}

ディスクが初期化されたかどうかは結構厳密に聞いてくるようなので、isInitializedという変数を用意し初期化されたかを記憶しておきます。そして、disk_detached関数を作り、main.c側でUSBメモリーが取り外されたことがわかったらこの関数を呼び出し初期化されていないことを伝えます。

USBメモリーのアクセス関係の関数はusb_host_msd_scsi.cに入っているようです。
disk_initialize関数とdisk_status関数はUSBHostMSDSCSIMediaInitialize関数とUSBHostMSDSCSIMediaDetect関数をうまく使って処理しています。また、disk_writeとdisk_readはマルチセクタリード/ライトにも対応できるようなインターフェースをしていますが、USBHostMSDSCSISectorWrite関数とUSBHostMSDSCSISectorRead関数はマルチセクタリードに対応していないようなので、とりあえずfor文でそれをエミュレートしてあげています。デバイスドライバレベルでマルチセクタアクセスできたら相当速くなるんでしょうがね…。(少なくとも昔、SDカードのアクセスをやった時はそうでした。)

disk_ioctrl関数は一番めんどくさかったですね。
CTRL_SYNCはデバイスドライバレベルでキャッシュしてる場合にflushするための関数なようで、おそらくそういうことはしていないっぽいのでとりあえず何も処理をせずに成功を返すようにしました。CTRL_ERASE_SECTORは実装しなくても大丈夫なようなのでこちらも同様にそうしました。
GET_SECTOR_COUNTはセクタの個数だそうです。えっ、それってFATファイルシステム内読めばわかるでしょ?って思ったらどうもこれ、フォーマットするときに使うようですね。そりゃそうだ、フォーマットされていないディスクだったらディスクの容量はファイルシステムからじゃ読み出せませんからね。
…どうしろって言うんだ…。 usb_host_msd_scsi.cにはそれっぽいAPI無いですしね…。というわけで、フォーマットしなけりゃ問題ないっしょということで実装するのをやめました。はい。
GET_SECTOR_SIZEですが、普通は512バイトみたいですね。 最大セクタサイズと最小セクタサイズが同じだったらそれでいいっしょということでそれを返すようにしました。違ったらサポートしないってことで。結構適当な実装です。はい。ごめんなさい。

あとはget_fattime関数を定義してやる必要があります。これはファイルのタイムスタンプを書き込むための関数で、要するにRTCなどから読みだした現在時刻をここで返すようにしてやればいいわけです。
が、NTPだのGPSだの何だので時計合わせするようなハードはあいにく持ち合わせていないので、適当にコーディングしていた時の時刻を返すようにしました。FATファイルシステムはタイムスタンプを32bitに収めるために、 秒なんか2秒毎にしか記録できないんですね。シフト演算子を組合せて時刻を定義しても良いでしょうが、ここはFATTIMEという共用体を作ってあげることにしました。こうやって各時刻要素の構造体とDWORDを共用体にすることで、構造体に入れた値をそのままDWORDとして読みだすことができます。すごく素朴でコストの小さい書き方でとても良いと思います。

これで最低限は動きそうな感じになったと思うんで、最後にmain関数側からこれを呼び出します。

    while(1)
    {
        LATAbits.LATA0 = 0;
        //USB stack process function
        USBTasks();

        //if thumbdrive is plugged in
        if(USBHostMSDSCSIMediaDetect())
        {
            FRESULT result;
            FATFS fatfs;
            FIL file;

            deviceAttached = TRUE;
            LATAbits.LATA0 = 1;

            f_mount(&fatfs, "0:", 0);
            result = f_mkdir("0:testdir");
            if((result == FR_OK) || (result == FR_EXIST)) {
                result = f_open(&file, "0:testdir/file.txt", FA_CREATE_ALWAYS | FA_WRITE);
                if(result == FR_OK) {
                    char szText[32] = "Hello, FatFs!";
                    UINT bw;

                    f_write(&file, szText, strlen(szText) * sizeof(char), &bw);

                    f_close(&file);
                    f_mount(NULL, "0:", 0);

                    while(deviceAttached == TRUE)
                        USBTasks();
                }
            }
        }
    }

ディレクトリの生成の試験も兼ねて、ドライブの中にtestdirというディレクトリを生成し、その中にファイルを生成しています。


そして無事、ファイルを書き込むことができました。
Microchipのライブラリと違い、タイムスタンプもちゃんと入っています。

さらに、USBドライバとFatFsを入れた時点でのプログラムメモリの使用率はなんと54%なんですね。
プログラムメモリを78%も占有することになったMicrochip製のライブラリよりも相当コンパクトだと言えるでしょう。さっすがFatFsです。

2014年6月17日火曜日

PIC32MX250F128BでUSBメモリーに書き込む

前回につづいてPIC32MX250のお話です。

前回はプログラムのビルドが終わりました。今度は実際に回路を作ってみましょう。
必要最低限の回路です。こんな感じです。



セラロックは8MHzを付けています。PIC32MXのオシレーターのブロック図は結構複雑になっていて、USBは8MHzを1/2プリスケーラを通して4MHzにした後、24倍PLLを通して96MHzにし、さらに1/2ポストスケーラを通して48MHzにしています。CPUのメインクロックは1/2プリスケーラで4MHzにした後PLLで15倍の60MHzにして供給しています。結構高速で動いてくれるんですね。
PICは3.3Vで動きますが、USBは5Vを供給してやる必要があります。そのため、5VのACアダプタを使って、USBには5V、PICにはレギュレーターを介して3.3Vを供給してやっています。Vbusは5Vトレラントですが(そうじゃなかったら困る)、PIC32MXのピンは5Vトレラントのピンがだいぶ限られているので、もっと実用的なアプリケーションを開発するときには注意する必要がありそうですね。

LEDは必要ないって言えば必要ないですが、LEDデバッグをするために用意しています。


実物はこんなかんじになっています。Cサイズのユニバーサル基板に適当に組み上げました。

プログラムでは、USBメモリーを挿すとLEDが点灯し、抜くと消灯するようにしてみました。USBメモリーを挿してLEDが点灯してから引っこ抜き、パソコンで中身を確認します。


見事にファイルが書き込まれていました。めでたしめでたし。

一番苦労したのはPICの配線ですかね(汗
USB周りのピン(Vbus、Vusb3v3など)をどうつなぐのかを回路図とかでパッと説明してるサイトがパッと出てこずに少し苦労しました。でも、もう回路図は上に載せておいたのですんなりと動かせるかと思います。

このデモプログラムにはタイムスタンプを書き込む機能が付いていないようで、上のように更新日時が空になっています。メモ帳では読めるみたいですが、TeraPadは受け付けてくれませんでしたね。この辺、改良の余地はありそうです。

USBメモリーがマイコンで使えるとなると、ロガーなどの用途には一気に使いやすくなりますよね。
その他、ENC28J60と組合せてHTTPサーバーとか作れるかも。
夢は広がりんぐです。