Processing math: 100%

2014年6月27日金曜日

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

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



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

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

1
2
3
<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を修正してやります。
ついでに、 テーマのリソースを指定してやる必要があるので、それを追加します。

1
2
3
4
5
6
7
8
<Application x:Class="RibbonTest.App"
             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で記述します。こんなかんじです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<Fluent:RibbonWindow x:Class="RibbonTest.Views.MainWindow"
        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が出てきます。
やっているのはそれだけのことです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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#なのでこのコードを見れば何をやっているかはわかると思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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で楕円を描画するのに必要なのは幅、高さ、左上の座標なので、直径と左上の座標を計算しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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言語で書いたとしましょう。

1
2
3
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言語でこのオブジェクト指向に近い書き方をすることにしてみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
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はこんな感じになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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構造体の配列を持っており、こんな感じで初期化しています。

1
2
3
4
5
6
7
//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です。

1
2
3
4
5
6
7
8
9
void ProcessLCD()
{
    static uint8_t LastPageIndex;
 
    if(pPages[PageIndex]->DrawLCD != NULL)
        pPages[PageIndex]->DrawLCD(LastPageIndex != PageIndex);
 
    LastPageIndex = PageIndex;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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の定義をしてあげます。こんなかんじになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#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関数側からこれを呼び出します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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サーバーとか作れるかも。
夢は広がりんぐです。

2014年6月16日月曜日

PICkit3を改造するお話

さて、PIC32MXをいじるにあたって、PICkit3のハードウェアバグについてちょっと触れておきましょう。
前にPIC32MX220F032Bをいじったときに発生した問題です。
はい、何度設定を見なおしても全然PICkit3が正常にPICを認識してくれないんですね。
いろいろググりまくった結果、PICkit3のハードウェアバグという結論に。

PICkit 3 の低電圧動作に関する改修

ある一定のアセンブリ番号以前のPICkit3だと、保護用か何かの抵抗が悪さしちゃってうまく書き込めないらしいです。なので、PICkit3をばらして当該抵抗をスズメッキ線でバイパスしてあげました。

あくまでも改造は自己責任でどうぞ。

PIC32MX250F128BでUSBホストをやる

さて、NTP時計の次の連載になる気がしているPIC32MX250の記事をちょっと書こうと思います。



最近、秋葉原の某電子部品店で取り扱いが始まったPIC32MX250ですが、USB-OTGが付いております。そもそもUSBとはパソコン(ホスト)とデバイスが通信するためのものですが、デバイス同士の通信ニーズも出てきておりますので(スマホとUSBメモリー、カメラとプリンターの接続などなど)USB-OTG(On The Go)っていうのが定義されたらしいです。PICはパソコンじゃないのでそもそもはデバイス側になるのですが、OTGでそれがホストとして動作させられるってことですね。(細かい話はわかってないので違ってたらごめんなさい)

PICでUSBホストをやるとなると、やはりメジャーなのはPIC24FJ64GB002/004とかなのでしょうが、とにかくこれの入手性が悪いんですよね。秋葉原を探しまわっても見つけたことがありません。多分、DigiKeyとかじゃないと手に入らないんでしょうね…。


前置きはこれくらいにしておいて、早速プログラムのビルドに入ってみましょう。
ビルドできる見込みが立ったらハードを作るという流れで。

例によってMicrochip Libraries for Applicationsを使います。

Microchip Libraries for Applications

めんどくさいので、ENC28J60を動かすときに使った2013/06/15のバージョンでビルドをしていきます。使うのはこのデモです。

microchip_solutions_v2013-06-15\USB\Host - Mass Storage - Simple Demo

しかしこのデモ、どうもデフォルトのターゲットはPIC24FJ256GB106のようですね。しかし、PIC32MXシリーズで動かすことも想定された設計がされており、おそらくPIC32MXでもある程度容易にビルドできるだろうということがわかります。



このように左側のCategoriesペインに多くのビルドプロファイルがありますね。PIC32MX250こそ無いものの、PIC32MXシリーズがあるのでうまくいく可能性は結構あります。

プロジェクトに以下のようなファイルを登録しました。


ファイルがどこにあるかは、もとのデモプロジェクトを見ればわかります。
HardwareProfile.hとかその辺はいじらなければいけないオーラがものすごく出ていますが、まあとりあえずビルドを通すことを目標にして今回はパスしておきます。

例によってIncludeディレクトリのパスを通しておく必要があります。microchip_solutions_v2013-06-15\Microchip\Includeと、プロジェクトのディレクトリを通しておけば大丈夫です。


この他に重要なのは、ヒープ領域の容量を設定する必要があることです。
ヒープ領域とは、いわゆる関数の呼び出しやブロックの変数確保で使われるスタック領域とは違い、malloc等でメモリを動的に確保するときに使われるメモリ領域です。デモプログラムのどこかでmallocされてるってことなんでしょうね。(ファイルシステムのところの気がしています)

ヒープ領域はリンク時に用意されるものなので、xc32-ldの設定になります。デモプログラムでは2000bytes用意されていたので、とりあえず2000bytesにしておきましょう。


これでビルドが通りました。めでたしめでたし。
でもこれだけでもうプログラムメモリの78%を使ってしまうんですね…。

SPI接続グラフィック液晶のAQM1248Aを動かす

さて、とある電子部品屋で買える激安小型グラフィック液晶のAQM1248Aですが、これをNTP時計の補助表示器として使いました。

多分、これをSPIで制御するの何のって話は、付属のデータシートを見ればだいたいわかりますし、ググればいっぱいその記事が出てくると思うので細かい話は割愛しようと思います。

ところで、とてもとても試行錯誤したのですが、ハードウェアSPIが上手く動いてくれなかったんですね…。

PIC24FJ64GA004は2つのSPIを持っているので、1つはENC28J60、もう1つはSPI液晶に割り当てればいいんですが、なんかどうも動いてくれないんですね…。
原因はよくわからないのですが、PIC32MX220F032Bなら何も問題なく動いてくれるんですよねえ。PIC24FJ64GA004のSPIモジュールには「受信は使わない」っていうオプションが無いからそれが原因なんでしょうか。よくわかりません。

仕方ないので、ソフトウェアでSPIを実装しました。まあ、受信しないのでとても実装は楽でした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void SendSPI(uint8_t data, BOOL IsData)
{
    int i;
 
    LED7SEG_DisableInterrupt();
 
    LCD_CS_IO = 0;
    LCD_RS_IO = IsData;
 
    for(i = 0; i < 8; i++) {
        LCD_SDO_IO = (data & 0x80) != 0;
        LCD_SCK_IO = 0;
        LCD_SCK_IO = 1;
        data <<= 1;
    }
    LCD_CS_IO = 1;
 
    LED7SEG_EnableInterrupt();
}

あと、なぜだかSPIの送信中に7セグの割り込みが入ると7セグの表示がバグるんですねえ。本当に不思議ですねえ。液晶ががバグるんじゃなくて7セグの表示がバグるんです。LATxレジスタを叩いてるからビット操作の問題も起きないはずだし、本当に不思議です。

XCコンパイラって例えばLATBbits.LATB5 = 1っていう制御、実際は一回LATBをワーキングレジスタにコピー→OR演算を実行→LATBに書き戻すっていう3ステップの処理とかをしているんですかね。それだったら、例えばLATBをWregに読みだした時点で割り込みが入ってそこでLATBをいじったとしても、割り込みから復帰した後には割り込みが起こる前のLATBに演算をした結果がLATBに入ってしまいます(マルチスレッドプログラミングにおけるクリティカルセクションの話題のテンプレ的な例)。しかし、PIC24FJ64GA004はビット操作命令を持っています。コンパイラがこれをビット操作に置き換えてくれれば1命令で終わるので、割り込みに関するそのような問題は起こりません。確かに構造体を使ったビット操作は、1ビットに限らず任意のビット数の変数を構造体内に確保できる文法です。なので、1ビット分の制御でもわざわざOR/AND演算を用いてくる可能性は十分に考えられます。でも、これを解明しようとしたら…HEXを逆アセンブルしてPIC24シリーズのアセンブリ読まなきゃいけないの…ちょっと無理ん…。 PIC18までならアセンブリ開発はよくやってましたが…

プログラミングで「なぜだかわからないけど」は本当は良くないんでしょうが、 現状これでしっかりと動いてくれています。SPIが動かない原因や割り込みでバグる原因がちゃんと分かる人いたら教えて下さい><




液晶には2種類のフォントを搭載しました。
この液晶は縦8bitのデータを1バイトのデータとして送るので縦は8ピクセルごとの制御が便利です。というわけで縦8ドットのフォントを用意したのですが、とても小さすぎました。
なので、縦16ドットのフォントも用意しました。フォントだけでプログラムメモリ20%くらい持って行かれました(汗

そういえば、NTP時計を作製するにあたって初めて知ったのですが、 C言語にはstrftimeっていう関数があるんですね。sprintf関数があれば何かのデータをフォーマットしてテキスト化するのに困りませんが、strftimeはtm構造体をフォーマットするのにあたっていくつか便利な感じになっています。
例えば、写真の液晶の上段は

1
strftime(text, sizeof(text) / sizeof(char), "%Y/%m/%d (%a)", &Time);

こんな関数の呼び出しでフォーマットしています。便利ですねえ。

2014年6月13日金曜日

SNTP.cの改造

さて、今回はNTP時計の真髄であるSNTP.c周りの改造についてお話したいと思います。

このファイル、眺めてて真っ先に思うことは「これ、正確な時間じゃない!!!!」ってことなんですね…。はい。改造前のSNTP.cには様々な問題点が含まれています。
しかし、これは全部PIC18シリーズでも動くことを念頭に置いて設計した結果なんです。すなわち、処理を徹底的に軽くして、とりあえずだいたいの現在時刻が分かればいいな、くらいのものだってことなんですね。それはそれでコーディングのポリシーがあるから悪いことだとは思いません。

しかし、今回このSNTPパケットの解釈をし、時計合わせするプロセッサはPIC24FJ64GA004です。PIC18シリーズに比べたらかなりの処理能力の向上が見込めます。なので、多少重い処理でも、より正確な時刻を求めて計算させることくらいきっと大丈夫!

というわけで、まずはSNTP.cの改造をするために問題点を見極めてみましょう。
まず見た場所は、SNTPの主たるネットワーク処理をしている関数、SNTPClient関数です。ここでは、呼び出される度に何らかの処理をし、条件を満たしたらSNTPStateという変数を適宜変えていきます。そうすると、再びSNTPClient関数が呼ばれたときにそのSNTPStateをもとにswitchし、特定の処理をして、またSNTPStateの値を更新していくんですね。C#で言うyield的なやつ?
SNTPにはUDPが使われていて、NTPサーバーに対して時刻を要求するパケットを送ったら、時刻のUDPパケットが返ってくるわけですね。なので、その受信したパケットを処理するところが、時計合わせの最もコアになる部分なわけです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
case SM_UDP_RECV:
    // Look for a response time packet
    if(!UDPIsGetReady(MySocket))
    {
        if((TickGet()) - dwTimer > NTP_REPLY_TIMEOUT)
        {
            // Abort the request and wait until the next timeout period
            UDPClose(MySocket);
            //dwTimer = TickGetDiv64K();
            //SNTPState = SM_SHORT_WAIT;
            SNTPState = SM_HOME;
            MySocket = INVALID_UDP_SOCKET;
            break;
        }
        break;
    }
     
    // Get the response time packet
    w = UDPGetArray((BYTE*) &pkt, sizeof(pkt));
    UDPClose(MySocket);
    dwTimer = TickGetDiv64K();
    SNTPState = SM_WAIT;
    MySocket = INVALID_UDP_SOCKET;
     
    // Validate packet size
    if(w != sizeof(pkt))
    {
        break;   
    }
     
    // Set out local time to match the returned time
    dwLastUpdateTick = TickGet();
    dwSNTPSeconds = swapl(pkt.tx_ts_secs) - NTP_EPOCH;
    // Do rounding.  If the partial seconds is > 0.5 then add 1 to the seconds count.
    if(((BYTE*)&pkt.tx_ts_fraq)[0] & 0x80)
        dwSNTPSeconds++;
 
    break;

Microchip製のコードはこんな感じになっています。
UDPGetArray関数でUDPの受信パケットを読みだして受信サイズがちゃんとUDPパケットのサイズと合っているか確認しています。そのあとは、NTPパケットはビッグエンディアンなのでそれをリトルエンディアンに変換しています。
NTPパケットのタイムスタンプは1900年1月1日午前0時からの秒数を64bitの固定小数点型で返してきます。その64bitのうち、上位32bitが整数部で、下位32bitが小数部です。C言語の多くの処理系では現在時刻を1960年1月1日0時からの秒数で管理しており、XC16も例には漏れていないので、NTP_EPOCHで表されるその差を引いてやっています。
そして、小数部の最上位ビットを見て、そこが1なら四捨五入ということで秒を1秒繰り上げています。
はい、このライブラリは秒以下の精度を管理していないんです。せっかくのNTP時計ですからミリ秒単位で正確な時刻で刻みたいですよね。というより、コロンを0.5秒ごとにON/OFFを切り替えているので必然的にミリ秒単位の精度が必要になってきます。この点は改善が必要ですね。

ところで、NTPパケットはもちろん、NTPサーバーを出てからインターネットを経由して手元まで届きます。NTPサーバーからNTP時計までの間にはもちろん伝送経路の遅延があり、すなわち、その間に時間が進んでしまいます。なので、手元にパケットが到着した時点でそれはもはやすでに正確な時刻ではないのです。
pkt.tx_ts_secsメンバは、まあ見た目の通り、NTPサーバーがパケットを送信する瞬間の時刻です。NTPパケットには、これ以外にもう1つサーバーが送ってくる時刻情報が入っています。それは、NTPサーバーにパケットが到着した時の時刻です。すなわち、NTP時計側で時刻要求パケットを送信した時刻とその返事を受け取った時刻を記録していれば、NTPサーバーがパケットを受信してから返信するまでの時間を差し引いて1/2にすれば伝送経路の遅延時間がわかりますね。
そうやって、さらにより正確な時計合わせをすることができるようになります。ここまで説明した通り、このプログラムはNTPサーバーをパケットが出発した時刻に合わせた正確じゃない時計合わせですが、まあ、もともと1秒単位の時計合わせしかしていない時点でそんな伝送遅延にこだわるようなレベルじゃないですし、これはこれで軽い処理としての妥協点としては別に(正確な時計として使わないのならば)問題ないレベルでしょう。

もう一つ、このプログラムには問題点があります。
このプログラムでは、NTPパケットを受信したときにそのパケットに示されていた時刻をグローバル変数のdwSNTPSecondsに保管し、それと同時にGetTick関数で現在のTMR1のカウント値(32bit)を同じくグローバル変数のdwLastUpdateTickに保存しています。
後にSNTPGetUTCSeconds関数を呼び出したときに 現在のTickから前回のNTPパケットの取得時のTickを引くことで時間差がわかるので、それをNTPパケットの取得時の時刻に足して返すという仕組みになっています。
はい、 32bitなんですよ、32bit。TMR1のカウント値は、プリスケーラやポストスケーラは何も挟んでいないので、32MHzで動いているPICでは1秒で16メガ増えます。すなわち、5分弱でオーバーフローしてしまうんですね。
というわけで、SNTPGetUTCSeconds関数にはその対策が取られています。この関数が呼ばれたときはdwSNTPSecondsを現在の時刻に変更し、それと同時にdwLastUpdateTickも更新しているのです。
しかし、この仕様では「dwLastUpdateTickは変数の名前に反して前回NTPサーバーに接続した時刻ではない」「SNTPGetUTCSecondsを5分弱以上呼び出さないとバグる」という問題が残ることになります。前者は気持ちの問題ですが、後者は致命的ですね。もちろん、main関数側の実装でカバーすることはできますが、こういう仕様は私は大っ嫌いです。
Tick.cのほうの記事でも触れましたが、Tick.cではTickを48bit値でカウントしているので、SNTP.cのほうの改造だけでさらに16bit増やすことができます。しかし、それでも203.6日間SNTPGetUTCSeconds関数を呼ばなければオーバーフロー起こしてバグります。長いって言えば長いですが、到底NTPが2036年問題を起こすときやC言語の処理系が2038年問題を起こすときには及びません。だから、Tick.cのほうでTickを64bitで管理するように変更したんです。それなら36,000日以上もちます。

はい、まとまりました。
Microchip謹製のライブラリの問題点は
  • 小数以下を四捨五入している程度の精度
  • 伝送遅延を考慮していない実装
  • SNTPGetUTCSeconds関数を頻繁に呼ばないとバグる仕様
の3点のわけですね。
今回の改造では、これらを改善するコードを追加しましょう。



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
case SM_UDP_RECV:
    // Look for a response time packet
    if(!UDPIsGetReady(MySocket))
    {
        if((TickGet()) - dwTimer > NTP_REPLY_TIMEOUT)
        {
            // Abort the request and wait until the next timeout period
            UDPClose(MySocket);
            //dwTimer = TickGetDiv64K();
            //SNTPState = SM_SHORT_WAIT;
            SNTPState = SM_HOME;
            MySocket = INVALID_UDP_SOCKET;
            break;
        }
        break;
    }
     
    // Get the response time packet
    w = UDPGetArray((BYTE*) &pkt, sizeof(pkt));
    UDPClose(MySocket);
    dwTimer = TickGetDiv64K();
    SNTPState = SM_WAIT;
    MySocket = INVALID_UDP_SOCKET;
    bForceSync = FALSE;
     
    // Validate packet size
    if(w != sizeof(pkt))
    {
        break;   
    }
 
    // Set out local time to match the returned time
    dwLastUpdateTick = TickGet();
    dwSNTPSeconds = swapl(pkt.tx_ts_secs) - NTP_EPOCH;
    // Do rounding.  If the partial seconds is > 0.5 then add 1 to the seconds count.
    if(((BYTE*)&pkt.tx_ts_fraq)[0] & 0x80)
        dwSNTPSeconds++;
 
    {
        QWORD now,  qwReceive, qwTx, qwDelay, qwPeriod;
         
        now = MillisecondToNTPTimestamp(SNTPGetUTCMilliseconds());
        qwPeriod = now - qwLastUpdateNTPTimestamp;
        qwLastUpdateTick = TickGetQWord();
 
        qwReceive = PacketToNTPTimestamp(pkt.recv_ts_secs, pkt.recv_ts_fraq);
        qwTx = PacketToNTPTimestamp(pkt.tx_ts_secs, pkt.tx_ts_fraq);
        qwDelay = (now - qwLastSendNTPTimestamp - (qwTx - qwReceive)) / 2;
 
        qwLastUpdateNTPTimestamp = qwTx + qwDelay;
 
        dOscillatorError = (double)((LONGLONG)(now - qwLastUpdateNTPTimestamp)) / qwPeriod;
        dwSyncCount++;
    }
 
    Stratum = pkt.stratum;
 
    break;

受信部分はこんな感じにしました。
にしてもC言語のcase文は根底の考え方がラベルとジャンプで、各caseの中はブロックにする必要が無いので変数の定義ができずめんどくさいですね。
後半部分に改造した部分が主に入っていて、特にwhileとかifとかにもくっついていないブロックのところです。
タイムスタンプは64bitで保管し、整数部32bit、小数部32bitの固定小数点型として保存しています。はい。NTPのパケットそのままです。ただし、リトルエンディアンに変換はしています。
固定小数点は特に固定小数点型みたいなものを用意しなくても、足し算引き算に関しては整数の演算として正しい演算ができますね。というわけで、この形で伝送遅延qwDelayを計算しています。そして、送信された時刻+伝送遅延を現在時刻として保存しています。

同時に、オシレーターの誤差も計算しておいています。
現在の時刻とNTP時計によって得られた時刻情報との差を同期間隔で割っています。
また、そのような計算をしている都合上、2回目の同期以降じゃなければその誤差は有効な計算とならないので、dwSyncCountで何回同期されたかをカウントしています。

その他、Stratumの保存も行っています。StratumはNTPサーバーの時刻の純度とでも言いましょうか。NTPサーバーの時刻は別のNTPサーバーによって合わせることもできるので、そのようにして合わせた場合はStratumを増やして純度が下がっていることを示す仕組みが取られています。Stratumが1が最も正確な時計で、例えば原子時計やGPSをもとに時計合わせされたNTPサーバーがそれに値します。

ちなみに、 qwLastSendNTPTimestampはSM_UDP_SENDの中で保存しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case SM_UDP_SEND:
    // Make certain the socket can be written to
    if(!UDPIsPutReady(MySocket))
    {
        UDPClose(MySocket);
        SNTPState = SM_HOME;
        MySocket = INVALID_UDP_SOCKET;
        break;
    }
 
    // Transmit a time request packet
    memset(&pkt, 0, sizeof(pkt));
    pkt.flags.versionNumber = 3;    // NTP Version 3
    pkt.flags.mode = 3;                // NTP Client
    pkt.orig_ts_secs = swapl(NTP_EPOCH);
    qwLastSendNTPTimestamp = MillisecondToNTPTimestamp(SNTPGetUTCMilliseconds());
 
    UDPPutArray((BYTE*) &pkt, sizeof(pkt));   
    UDPFlush();   
     
    dwTimer = TickGet();
    SNTPState = SM_UDP_RECV;       
    break;

このようにして大まかな機能の拡張はできたので、後はこの拡張したデータを読み出す系の関数をたくさん実装してあげればいいですね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
QWORD SNTPGetLastUpdateUTCMilliseconds(void)
{
    return NTPTimestampToMillisecond(qwLastUpdateNTPTimestamp);
}
 
QWORD SNTPGetUTCMilliseconds(void)
{
    QWORD qwDeltaMs = (TickGetQWord() - qwLastUpdateTick) * 1000 / TICK_SECOND;
     
    return  SNTPGetLastUpdateUTCMilliseconds() + qwDeltaMs;
}
 
void UTCMillisecondToLocaltime(QWORD utcms, int diffmin, struct tm *pTime)
{
    if(pTime != NULL) {
        time_t tick = ((DWORD)(utcms / 1000) + diffmin * 60) & 0x7FFFFFFF;
        struct tm *ptm = localtime(&tick);
        memcpy(pTime, ptm, sizeof(struct tm));
    }
}
 
char *GetSNTPServerName(char *ptr, int length)
{
    length = (length > NTP_SERVER_LENGTH) ? NTP_SERVER_LENGTH : length;
 
    if((ptr == NULL) || (length <= 0))
        return NULL;
 
    strncpy(ptr, NtpServer, length - 1);
    ptr[length - 1] = '\0';
 
    return ptr;
}
 
void SetSNTPServerName(const char *ptr)
{
    if(strlen(ptr) < NTP_SERVER_LENGTH) {
        strcpy(NtpServer, ptr);
         
        bForceSync = TRUE;
    }
}
 
DWORD GetSNTPQueryIntervalSec()
{
    return NtpQueryIntervalSec;
}
 
void SetSNTPQueryIntervalSec(DWORD sec)
{
    NtpQueryIntervalSec = sec;
}
 
double GetOscillatorError()
{
    if(dwSyncCount >= 2)
        return dOscillatorError;
    else
        return 0;
}
 
BYTE GetSNTPStratum()
{
    return Stratum;
}

この関数に含められているので気づいた方もいるかもしれませんが、接続先のNTPサーバーの変更と、同期間隔の変更も可能にしてあります。
そのために、UDPOpenEx関数の呼び出しパラメーターを多少変えたり、SM_WAITでのINVALID_UDP_SOCKETへの変遷条件を若干変更したりしています。

こんなもんですかね。

2014年6月11日水曜日

7セグメントLEDの制御

こちらもネットワークとはあまり関係ない話です。

NTP時計では6桁分の7セグメントLEDを制御しています。
複数桁の7セグを制御するときは、通常、ダイナミック点灯制御と呼ばれる制御が使われます。この制御では制御のリアルタイム性が非常に重要になってくるので、タイマ2を使った割り込みで制御を行っています。

ところで、NTP時計ではこの7セグの明るさを変えたいというニーズが出てきます。
時計は24時間動き続けるもの。夜、部屋を消灯しても煌々と光り続けていたらとても目障りです。

なので、ダイナミック点灯制御に加えてPWM制御も行い、明るさを変えられるようにしました。さらに、CdSを使い、環境の明るさを取得し、設定したしきい値で自動的に7セグの明るさを変えるようにしています。

ところで、この話題をTwitterでしていたら、興味深い情報を教えてくれた人がいました。
実は昔、同じように7セグで時計を作ったことがあって、それにも同様にPWMによる明るさ制御はしていました。しかし、PWMのデューティー比が小さいうちはデューティー比を変えると目に見えて明るさが変わるのですが、デューティー比が大きくなるとまあ変わってはいるのでしょうが、そんなに大きく明るさが変わっているようには見えません。

3乗にどういう意味があるのかはわかりませんが、そのような経験から、明るさの設定値が大きくなればなるほどデューティー比を大きく変えるという相関は理に適っていると言えます。


次に、ダイナミック点灯制御の実装を考えます。
もともとこのPWMの明るさ制御をする前はタイマ2の割り込みで点灯させる桁を切り替えていましたが、PWMするとなるとさらに例えば256段階制御するならこの1/256の周期で割り込みを入れなければなりません(と思い込んでいた)。

割り込み周期は0x2000サイクルだったので、1/256にすると0x0020サイクル…32サイクル…。
当然、割り込み処理をしている間に次の割り込みが入り、永遠に割り込みが終わらない事態になってしまいました。

はい、お分かりの方も多いとは思いますが、割り込みが掛かったときにTMR2の値を適当に設定してやれば、(0xFFFF-TMR2の設定値) サイクル後に割り込みが入ることになりますね。
PIC24Fシリーズの割り込みは割り込みが発生する値をPRxレジスタに設定すればそこで割り込みが起こるので、こういう使い方をすっかり忘れていましたね。PIC18まではよくやっていたのに…。


というわけで、こんな感じになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define PWM_PERIOD ((uint16_t)0x2000)
 
void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;
 
    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);
 
    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;
 
    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);
 
    Brightness = value;
}

消灯はしたくないので、0~255の設定値を1足して1~256にしてあります。そして、それを3乗して256の3乗で規格化したうえで、PWM_PERIODをかけています。PWMの周期です。
そして、TMR2のLEDがONになるときのスタート値とOFFになるときのスタート値を計算し、保存しておいています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void _ISR __attribute__((__auto_psv__)) _T2Interrupt(void)
{
    static uint8_t cnt;
    IFS0bits.T2IF = 0;
 
    if(cnt++ & 0x01) {
        TMR2 = PWMOnStart;
 
        Show7SEG();
        LED_COLON = Colon;
    } else {
        TMR2 = PWMOffStart;
 
        LATA &= ~(MASK_7SEG_RA | MASK_7SEGTR_RA);
        LATB &= ~(MASK_7SEG_RB | MASK_7SEGTR_RB);
        LATC &= ~(MASK_7SEG_RC | MASK_7SEGTR_RC);
        LED_COLON = 0;
    }
}

割り込みはこのようにして、カウンタをずっとインクリメントしていき、下1bitが1のときか0のときかでONにする処理、OFFにする処理を使い分けています。


余談ですが、これ、設定値が小さい時は全然明るさ変わって見えないんですよね…。
3乗を導入したから仕方ないって言えばそうなんですが、 例えば、明るさ設定値+1が32以下のときのPWMのONサイクル数を示したのが以下の図です。

はい。12以下に至っては、ONサイクル数がゼロでずーっときちゃってますね…。
多分、13以降もある一定の値までは、割り込みルーチンを抜けるのにこれ以上時間が掛かってしまい、実質的に明るさがほとんど変わらないゾーンが出てくるはずです(それでもOFFサイクル数は若干変化するので少しは変わって見える?)。
うーんこの微妙な感じ。
PWMの周期を大きくすればここらへん明確に出てくるんでしょうが、そうするとダイナミック点灯制御がちらついて見えてしまいます。 となると解決策はマイコンを高速で動かすこと…もうこれ以上速度上がらないよ!

というわけで、まあ、結局3乗導入したけどそれはそれ残念なことが起こっちゃったテヘペロッ☆というオチでした。

Tick.cの改造

さて、NTP時計ですが、このネットワーク周りのライブラリにはTick.cというファイルがあります。
これは、タイマ1を使用して時間をカウントするいわばストップウォッチみたいなライブラリで、SNTPライブラリもこれを使用することで今何時かを管理しています。

しかし、このカウンタは6バイト構成となっており、PICを32MHzで動かすと下2バイトくらいが秒数の小数部分のカウントになり、秒としては実質的に4バイト値となってしまいます。

もともとNTPには2036年問題があり、多くのC言語処理系には2038年問題があるので、実質4バイトでもそんなに困らないような気もしなくもないです。が、6バイト値を読みだすためにライブラリでは結構めんどくさい処理とかしている(PIC18シリーズのXC8コンパイラでは64ビット整数がサポートされていないため)のも残念なので、まあせっかくなので64bit化してしまいましょうという話です。

お話はとても簡単で、QWORD型(64bit符号なし整数:unsigned long longをtypedefしたもの)のカウンタを別途用意し、そっちでもカウントしてるってだけです。
完全に上位互換なので、6バイトのほうのプログラムを消しちゃって、64bit整数側からその旧来の関数を実装するリファクタリングをやっちゃってもいいんですが、まあ、デバッグとかめんどくさいんでプログラムメモリが足りなくなったら考えることにします…。

今回は図も何もないお話になってしまいました。ごめんなさい><

自動オシレーター調整

今回もNTP時計の話題ですが、一旦ネットワークの話から離れてみようと思います。

PICのオシレーターは様々な種類が選べます。時計にするならば、32.768kHzのクロックをタイマ1に付けて1Hzの割り込みを発生させたり、誤差±1ppmとかの超高精度発振器を使うなどの手もあります。
いずれにせよ、水晶発振子が比較的周波数の精度もよく、温度による変動も少ないので向いていると考えられます。

当初はそう思っていました。しかし、まあNTP時計ですしべらぼうに高精度なものを使う必要もないだろうということで、とりあえずセラロックを使っていました。

しかし、PIC24FJ64GA004には8MHzの高速RCオシレーターが内蔵されていて、これはOSCTUNレジスタを使って発振周波数を微調整することができます(内蔵RC発振以外は調整できません)。RCオシレーターは温度による誤差の変動が大きく、精度もあまり良くなく、一般に時計のようなものには向かないと言われています。しかし、NTP時計はNTPサーバーとの同期間隔と同期した時の時計のズレから誤差を知ることができるので、OSCTUNで微調整することができるRC発振がそういう意味で大きなメリットになります。周波数自動調整で、手動調整無しに精度が出せるならそれはそれで面白い!

そこで早速プログラムを書いてみました。

すると、どうもOSCTUNは値が1変わると0.4%くらいの時間のずれが生まれることがわかりました。0.4%って小さいようにも見えますが、実際は1時間で14.4秒もずれます。1日で6分近くずれます。時計としてはちょっと残念ですね。

そこで考えました。

「OSCTUNの値をPWM制御のように短期間で1つ隣の値と切り替え続ければ実質的にチューニング段数を増やせるんじゃね?」

はい。実際にはそのPWMの時間を制御するクロックがOSCTUNで動かされてしまうので正確に比例関係になるわけではありませんが、少なくとも大小関係くらいはちゃんと出るはずです。

というわけで、OSCTUNはもともと6bitなんですが、13bitまで増やしてしまいました。
7bit増えたので精度は128倍で、つまり、ずれは最大で1時間あたり0.1秒程度ということになりますね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <p24FJ64GA004.h>
#include <stdlib.h>
#include "SNTPx.h"
#include "OscTuning.h"
 
#define TUNE_BIT        13    //この関数としてのチューニングビット数
#define OSCTUN_BIT        6    //OSCTUNレジスタのビット数
#define OSCTUN_MASK        ((0x0001 << OSCTUN_BIT) - 1)
#define TUNE_LOWER_BIT    (TUNE_BIT - OSCTUN_BIT)
#define TUNE_LOWER_MASK    ((0x0001 << TUNE_LOWER_BIT) - 1)
#define TUNE_MAX        ((0x0001 << (TUNE_BIT - 1)) - 1)
#define TUNE_MIN        ((int16_t)(0x0001 << (TUNE_BIT - 1)) * (-1))
#define TuneValueToOscTune(tune)    (((uint16_t)tune >> TUNE_LOWER_BIT) & 0x3F)
 
#define ERROR_COEFFICIENT    ((250ul << TUNE_LOWER_BIT))    //OSCTUNが1変わるにはおよそ1/250=0.4%のずれが起きる
#define BIG_ERROR            (5.0 / ERROR_COEFFICIENT)    //TuneValueを1変えるに値する誤差の5倍の誤差あれば大きな誤差
 
int16_t TuneValue;
 
void Osc_SetTune(int16_t tune)
{
    tune = max(tune, TUNE_MIN);
    tune = min(tune, TUNE_MAX);
 
    TuneValue = tune;
}
 
int16_t Osc_GetTune()
{
    return TuneValue;
}
 
void Osc_ProcessOsc()
{
    static uint16_t cnt;
    static uint64_t LastLastUpdate;
    int16_t tune = Osc_GetTune();
    uint8_t osctun = TuneValueToOscTune(tune);
    uint64_t lastupdate = SNTPGetLastUpdateUTCMilliseconds();
 
    if((cnt++ & TUNE_LOWER_MASK) >= ((uint16_t)tune & TUNE_LOWER_MASK))
        OSCTUNbits.TUN = osctun;
    else
        OSCTUNbits.TUN = (osctun == 31) ? 31 : ((osctun + 1) & 0x3F);
 
    if(lastupdate != LastLastUpdate) {
        double OscError = GetOscillatorError();
 
        if((OscError > BIG_ERROR) || (OscError < -BIG_ERROR))
            Osc_SetTune(tune - (int16_t)(OscError * ERROR_COEFFICIENT + 0.5));
        else if(OscError > 0)
            Osc_SetTune(tune - 1);    // if oscillator is too fast, decrease OSCTUN.
        else if(OscError < 0)
            Osc_SetTune(tune + 1);    // if oscillator is too slow, increase OSCTUN.
 
        LastLastUpdate = lastupdate;
    }
}

そんなに厳密性を求めても仕方ないということで、タイマ割り込み等は使わずにmain関数のループの中でOsc_ProcessOscを呼び出すことで処理しています。

この拡張した擬似チューニング値を1変えると生まれる時間のずれが1/ERROR_COEFFICIENTです。時計合わせをした時のずれの絶対値がこのずれの5倍以内なら擬似チューニング値を1増やすか減らすかして微調整し、それより大きな誤差だったらOSCTUNが1変化すると0..4%ずれるという値に基づいて一気に目標値に近づけようとするような計算をしています。

これで1時間に1回時計合わせとかで実用上問題無いレベルの誤差に収まりました。


が、やっぱり温度依存性はすごいですね。
多分、室温が5℃くらい変わると擬似チューニング値が20くらい変わります。 0.01%/Kくらいのずれでしょうか。

詳しい温度との相関は取ってないですけど、一部からはRC温度計だの、NTP温度計だのと揶揄されていますw

2014年6月10日火曜日

ENC28J60をPICで動かす


さて、NTP時計を作るために真っ先にネットワーク関係のプログラムを構築しなくてはなりませんね。
ENC28J60はSPIインターフェースでマイコンと接続することができるので、Arduinoなどでもライブラリが作られていて、結構ポピュラーに使われているようです。PICに関して言えば、ENC28J60とともにMicrochipの商品ということもあり、Microchipが無料でライブラリを出しています。

Microchip Libraries for Applications

さて、早速ダウンロードと行きたいところですが、このページの英語を注意深く読むと、最新版のライブラリ(2013/12/20リリース)ではTCP/IPのサポートがされていないことが書いています。なので、このページ下部のLegacy MLAタブを選択して、2013/06/15のバージョンをダウンロードする必要があります。

さて、ダウンロードしてきてインストールすると、何やらいろいろなデモプログラム等がありますが、どこをどういじっていいのか見当も付きません。
しかし、いろいろと調べていると、さすがPICの神様。後閑さんのウェブページにかなり有用な記事があることがわかります。

TCP/IPスタック Ver4.5xの使い方

ここの「2.プログラム全体構成」の中にプログラムの構成のしかたが書いてあります。さすがです。

ライブラリの microchip_solutions_v2013-06-15\Microchip\TCPIP Stack にあるソースコードの中から、今回は以下のファイルを必要ファイルとしてプロジェクトに組み込みました。
ファイル名を見ればどれがどの役割を果たすプログラムかはなんとなくわかると思います。
また、Includeファイルのある場所にパスを通しておく必要があります。
MPLAB Xではプロジェクトのプロパティから左ペインでxc16-gccを選択し、Option categoriesをPreprocessing and messagesにするとインクルードディレクトリのパスを設定する項目が出てきます。

あとひとつ、最後にコンパイルのオプションを指定したインクルードファイルを用意する必要があります。用意するのは
  • HardwareProfile.h
  • TCPIPConfig.h
の2つです。 microchip_solutions_v2013-06-15\TCPIP\Demo App などから拾ってきて適当に改造すればいいですが、その「適当」がまた難しいところではあります。

HardwareProfile.hに関しては、ほぼ跡形もなく改造することになります。
めんどくさいんでもうコード貼っちゃいます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#ifndef HARDWARE_PROFILE_H
#define HARDWARE_PROFILE_H
 
#include "Compiler.h"
 
// Define a macro describing this hardware set up (used in other files)
#define EXPLORER_16
 
#define MAXIMUM_PIC_FREQ  (32000000ul)
 
 
// LED Definition
#define MIN_1  (LATAbits.LATA4)
#define LED_7SEG_D (LATAbits.LATA7)
#define SEC_1  (LATAbits.LATA8)
#define MIN_10  (LATAbits.LATA9)
#define LED_7SEG_C (LATBbits.LATB2)
#define LED_7SEG_A (LATBbits.LATB3)
#define SEC_10  (LATBbits.LATB4)
#define LED_7SEG_DP (LATBbits.LATB14)
#define LED_7SEG_E (LATBbits.LATB15)
#define LED_7SEG_B (LATCbits.LATC0)
#define LED_7SEG_F (LATCbits.LATC1)
#define LED_7SEG_G (LATCbits.LATC2)
#define LED_COLON (LATCbits.LATC3)
#define HOUR_1  (LATCbits.LATC4)
#define HOUR_10  (LATCbits.LATC5)
 
#define MASK_7SEG_RA ((uint16_t)0x0080)
#define MASK_7SEG_RB ((uint16_t)0xC00C)
#define MASK_7SEG_RC ((uint16_t)0x0007)
 
#define MASK_7SEGTR_RA ((uint16_t)0x0310)
#define MASK_7SEGTR_RB ((uint16_t)0x0010)
#define MASK_7SEGTR_RC ((uint16_t)0x0030)
 
// Switch Definition
#define SW_RED  (PORTAbits.RA10)
#define SW_BLUE  (PORTBbits.RB13)
#define SW_GREEN (PORTBbits.RB12)
#define SW_YELLOW (PORTBbits.RB11)
 
 
// These directly influence timed events using the Tick module.  They also are used for UART and SPI baud rate generation.
#define GetSystemClock()  (MAXIMUM_PIC_FREQ)   // Hz
#define GetInstructionClock() (GetSystemClock()/2) // Normally GetSystemClock()/4 for PIC18, GetSystemClock()/2 for PIC24/dsPIC, and GetSystemClock()/1 for PIC32.  Might need changing if using Doze modes.
#define GetPeripheralClock() (GetSystemClock()/2) // Normally GetSystemClock()/4 for PIC18, GetSystemClock()/2 for PIC24/dsPIC, and GetSystemClock()/1 for PIC32.  Divisor may be different if using a PIC32 since it's configurable.
 
// ENC28J60 I/O pins
#define ENC_CS_TRIS   (TRISBbits.TRISB10) // Comment this line out if you are using the ENC424J600/624J600, MRF24WB0M, or other network controller.
#define ENC_CS_IO   (LATBbits.LATB10)
// SPI SCK, SDI, SDO pins are automatically controlled by the
// PIC24/dsPIC SPI module
#define ENC_SPI_IF   (IFS0bits.SPI1IF)
#define ENC_SSPBUF   (SPI1BUF)
#define ENC_SPISTAT   (SPI1STAT)
#define ENC_SPISTATbits  (SPI1STATbits)
#define ENC_SPICON1   (SPI1CON1)
#define ENC_SPICON1bits  (SPI1CON1bits)
#define ENC_SPICON2   (SPI1CON2)
 
// LCD I/O pins
#define LCD_CS_IO   (LATBbits.LATB8)
#define LCD_RS_IO   (LATBbits.LATB7)
#define LCD_SDO_IO   (LATBbits.LATB5)
#define LCD_SCK_IO   (LATBbits.LATB6)
 
#endif // #ifndef HARDWARE_PROFILE_H

もとのデモプログラムのHardwareProfile.hを見てくれてもわかりますが、ほとんどがENC28J60と関係ない表記です。何のピンが何に割り当てられているかとか、そういったことが書いてあります。なので、このNTP時計のHardwareProfile.hもそんな感じになっています。
重要なのはENC28J60 I/O pinsのところで、そこにENC28J60と接続するのに使うレジスタと、ソフトウェアから制御するチップセレクトピン(CS)を定義しています。
ちなみに、もとのデモプログラムではこの中でコンフィグレーションビットの設定をしています。が、#ifdefはできるだけ避けたいのと、自分はいつもmain.cの中で定義していたので、我流にしたがってこちらからは削除しています。

つづいてTCPIPConfig.hですが、これはデモプログラムのものに手を加えるという形にします。
割と最初のほうにApplication Optionsという項目がありますので、ここを必要最低限のものを残してコメントアウトします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// =======================================================================
//   Application Options
// =======================================================================
 
/* Application Level Module Selection
 *   Uncomment or comment the following lines to enable or
 *   disabled the following high-level application modules.
 */
//#define STACK_USE_UART     // Application demo using UART for IP address display and stack configuration
//#define STACK_USE_UART2TCP_BRIDGE  // UART to TCP Bridge application example
//#define STACK_USE_IP_GLEANING
#define STACK_USE_ICMP_SERVER   // Ping query and response capability
#define STACK_USE_ICMP_CLIENT   // Ping transmission capability
//#define STACK_USE_HTTP2_SERVER   // New HTTP server with POST, Cookies, Authentication, etc.
//#define STACK_USE_SSL_SERVER   // SSL server socket support (Requires SW300052)
//#define STACK_USE_SSL_CLIENT   // SSL client socket support (Requires SW300052)
#define STACK_USE_AUTO_IP               // Dynamic link-layer IP address automatic configuration protocol
#define STACK_USE_DHCP_CLIENT   // Dynamic Host Configuration Protocol client for obtaining IP address and other parameters
//#define STACK_USE_DHCP_SERVER   // Single host DHCP server
//#define STACK_USE_FTP_SERVER   // File Transfer Protocol (old)
//#define STACK_USE_SMTP_CLIENT   // Simple Mail Transfer Protocol for sending email
//#define STACK_USE_SNMP_SERVER   // Simple Network Management Protocol v2C Community Agent
//#define STACK_USE_SNMPV3_SERVER   // Simple Network Management Protocol v3 Agent
//#define STACK_USE_TFTP_CLIENT   // Trivial File Transfer Protocol client
//#define STACK_USE_GENERIC_TCP_CLIENT_EXAMPLE // HTTP Client example in GenericTCPClient.c
//#define STACK_USE_GENERIC_TCP_SERVER_EXAMPLE // ToUpper server example in GenericTCPServer.c
//#define STACK_USE_TELNET_SERVER   // Telnet server
//#define STACK_USE_ANNOUNCE    // Microchip Embedded Ethernet Device Discoverer server/client
#define STACK_USE_DNS     // Domain Name Service Client for resolving hostname strings to IP addresses
//#define STACK_USE_DNS_SERVER   // Domain Name Service Server for redirection to the local device
//#define STACK_USE_NBNS     // NetBIOS Name Service Server for repsonding to NBNS hostname broadcast queries
//#define STACK_USE_REBOOT_SERVER   // Module for resetting this PIC remotely.  Primarily useful for a Bootloader.
#define STACK_USE_SNTP_CLIENT   // Simple Network Time Protocol for obtaining current date/time from Internet
//#define STACK_USE_UDP_PERFORMANCE_TEST // Module for testing UDP TX performance characteristics.  NOTE: Enabling this will cause a huge amount of UDP broadcast packets to flood your network on the discard port.  Use care when enabling this on production networks, especially with VPNs (could tunnel broadcast traffic across a limited bandwidth connection).
//#define STACK_USE_TCP_PERFORMANCE_TEST // Module for testing TCP TX performance characteristics
//#define STACK_USE_DYNAMICDNS_CLIENT  // Dynamic DNS client updater module
//#define STACK_USE_BERKELEY_API   // Berekely Sockets APIs are available
//#define STACK_USE_ZEROCONF_LINK_LOCAL // Zeroconf IPv4 Link-Local Addressing
//#define STACK_USE_ZEROCONF_MDNS_SD  // Zeroconf mDNS and mDNS service discovery

これは、主にStackTask.cでTCP/IP関係のプログラムを無限ループの中で呼び出していくときに使われます。定義されている項目のみ呼び出されるので、コメントアウトしたらそのモジュールは使用されません。容量の都合もありますし、不必要なものは徹底的に省いています。

そして、もう少し下へ行くと MY_DEFAULT_MAC_BYTE1~MY_DEFAULT_MAC_BYTE6というマクロが定義されています。これは、このネットワークアダプタのMACアドレスになります。
MACアドレスは6バイトのネットワークアダプタ固有のアドレスなので、世界中で重複したMACアドレスは存在しないことになっています。はい。まあ、自分は量産するわけでもないので適当に決め打ちで入力しちゃってます。デフォルトで入っているのはMicrochip社が購入しているMACアドレスですね。本気でMACアドレスを買おうとすると何百ドルもするようです…。


これで、だいたいのライブラリの構築は終わりました。

最後はmain関数の作成ですね。
ずばり、main関数はとてもシンプルになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
 __delay_ms(300);
  
 InitPeripheral();
 TickInit();
 InitAppConfig();
 StackInit();
 
 while(1) {
  StackTask();
  StackApplications();
  ProcessIO();
 }
 return (EXIT_SUCCESS);
}

非常にシンプルです。
起動してからは安定待ちとして適当に300msくらい待っています。気持ちの問題です。
InitPeripheral();はペリフェラルの初期化をしている関数です。ENC28J60で使うSPIのピン割り当てはここでする必要があります
TickInit();はライブラリのTick.cの中の関数です。詳しくは後日書くつもりですが、まあ時間を測るライブラリです。
InitAppConfig();はネットワーク関係でとても重要な関数ですので後述します。
StackInit();はStackTask.cの中にある、TCP/IPスタックを初期化する関数です。
StackTask();もStackTask.cにあり、TCP/IPスタックの処理をする関数です。
StackApplications();もStackTask.cにあり、SNTPとかのTCP/IPアプリケーションを動かす関数です。
ProcessIO();はENC28J60とは何も関係ないプログラムを処理するための関数です。

こんな感じっすかね。
InitAppConfigだけ詳しく説明しておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
APP_CONFIG AppConfig;
static ROM BYTE SerializedMACAddress[6] = {MY_DEFAULT_MAC_BYTE1, MY_DEFAULT_MAC_BYTE2, MY_DEFAULT_MAC_BYTE3, MY_DEFAULT_MAC_BYTE4, MY_DEFAULT_MAC_BYTE5, MY_DEFAULT_MAC_BYTE6};
static void InitAppConfig(void)
{
    // Start out zeroing all AppConfig bytes to ensure all fields are
    // deterministic for checksum generation
    memset((void*)&AppConfig, 0x00, sizeof(AppConfig));
 
    AppConfig.Flags.bIsDHCPEnabled = TRUE;
    AppConfig.Flags.bInConfigMode = TRUE;
    memcpypgm2ram((void*)&AppConfig.MyMACAddr, (ROM void*)SerializedMACAddress, sizeof(AppConfig.MyMACAddr));
    AppConfig.MyIPAddr.Val = MY_DEFAULT_IP_ADDR_BYTE1 | MY_DEFAULT_IP_ADDR_BYTE2<<8ul | MY_DEFAULT_IP_ADDR_BYTE3<<16ul | MY_DEFAULT_IP_ADDR_BYTE4<<24ul;
    AppConfig.DefaultIPAddr.Val = AppConfig.MyIPAddr.Val;
    AppConfig.MyMask.Val = MY_DEFAULT_MASK_BYTE1 | MY_DEFAULT_MASK_BYTE2<<8ul | MY_DEFAULT_MASK_BYTE3<<16ul | MY_DEFAULT_MASK_BYTE4<<24ul;
    AppConfig.DefaultMask.Val = AppConfig.MyMask.Val;
    AppConfig.MyGateway.Val = MY_DEFAULT_GATE_BYTE1 | MY_DEFAULT_GATE_BYTE2<<8ul | MY_DEFAULT_GATE_BYTE3<<16ul | MY_DEFAULT_GATE_BYTE4<<24ul;
    AppConfig.PrimaryDNSServer.Val = MY_DEFAULT_PRIMARY_DNS_BYTE1 | MY_DEFAULT_PRIMARY_DNS_BYTE2<<8ul  | MY_DEFAULT_PRIMARY_DNS_BYTE3<<16ul  | MY_DEFAULT_PRIMARY_DNS_BYTE4<<24ul;
    AppConfig.SecondaryDNSServer.Val = MY_DEFAULT_SECONDARY_DNS_BYTE1 | MY_DEFAULT_SECONDARY_DNS_BYTE2<<8ul  | MY_DEFAULT_SECONDARY_DNS_BYTE3<<16ul  | MY_DEFAULT_SECONDARY_DNS_BYTE4<<24ul;
}

このAppConfigという変数は、TCP/IPスタックライブラリの中でも使われてる変数です。グローバル変数はstaticじゃなければ、他のファイルからでもexternで修飾することで使えますね。すなわち、この変数を定義してやらないとコンパイルエラーになります。
まあ、デフォルトのIPアドレスとかMACアドレスとかをロードしてるだけなんで難しいことは無いと思います。

これで、とりあえずENC28J60が動くようになるはずです。

は~長かった。

ちなみに、PIC24FJ64GA004で、ここまででプログラムメモリの使用率が50%近くなるはずです。
恐ろしや…。

(余談ですが、コンパイルはXC16 v1.21でやっています。C18でコンパイルしようとしたらコンパイルが通らなくて、どうも調べたところによるとC18のバグとかいう話でした。本当かどうかは知りませんが…。)

Syntax Highlighterテスト

ブログにソースコードのハイライトを表示するツールにSyntax Highlighterっていうのがあるらしいですが、全然プレビューしてもハイライトしてくれません。

1
2
3
4
5
int main(void)
{
 printf("Hello, world!\n");
 return 0;
}

って思ったら、これ公開したらちゃんと表示してくれるのね…。

NTP時計

とあることをきっかけで知った、Microchip製のイーサーネットコントローラーのENC28J60というものを何かに使えないかな~と考えていたところ、NTP時計を作るということを思いつきました。
要するに、インターネットを介してNTPサーバーに接続し、常に正確な時刻を刻む時計です。

電波時計を使えばいいとか、GPSの時刻情報を使えばいいとか、いくらでもNTP時計じゃない正確な時計アイディアは出てきますし、とある電子部品店でNTP時計のキットを売っていたりするので、特に最適解的、もしくは新規性のあるアイディアではないですが、ENC28J60を使って遊ぶのを主目的として作ってみることにしました。

本来ならブログで作っていくところを紹介するのがいいんでしょうが、完成したこれを紹介したくてブログを開設した経緯もあって、もう完成してしまっています(汗
というわけで、完成した写真をいくつか紹介しましょう。


これが正面から見た写真です。専用基板を設計し、作成しました。時刻は7セグで表示し、細かな情報を表示するためにグラフィックLCDもつけています。真ん中に映ってるICがENC28J60ですね。ACアダプターで動きますが、一応停電時バックアップ用に充電用電池も付けています。(停電したらルーター止まるからネットにつながらねえだろ!とか、停電したって復活したらすぐに勝手に時計合わせが行われるから必要ねえだろ!なんていうツッコミは無しでwww
使われていないUSBポートが見えますが、これはただの電源供給用のUSBです。スマホとか充電できます。Wi-Fiから有線LANに変換する、いわゆる「イーサーネットコンバーター」っていう商品を使うことでこの時計を無線化することができますが、たいていのイーサーネットコンバーターはUSBで電源供給をする仕様になっているものが多いようで、そのために付けておきました。


裏を見るとこんな感じですね。左側に映ってる四角いのが、この時計のメインプロセッサです。PIC24FJ64GA004を使用しました。入手性がよく、小型で高性能なPICと言ったらやっぱりこれですね。
ズボラな性格なので、特に回路図とかを書くこともなく、そのまま適当にパターンを設計しちゃいました。そして、基板切削機(NCフライス的なやつ)を使って基板を作りました。
右上のプラネジの手前に写っているのが電源用のDC-DCコンバーターです。PICもENC28J60も3.3Vで動くので、5Vをこいつで落としてやってます。ダイナミック点灯制御用のFETとか、抵抗、コンデンサ等できる限りの部品を1608サイズや2012サイズのチップ部品を使って小型化を図っています(おかげではんだ付けが大変だった…

次回以降は各々の機能について設計や実装の観点から個別に特集していこうと思います。
まあ、暇があったらどんどん更新していくって感じで。

2014年6月9日月曜日

初めての投稿

ども、@EH500_Kintarouです。
なんかいろんなプログラミングをしてるうちに、いろいろ備忘録的に残しておきたくなったのですが、Twitterではすぐに流れてしまい、ウェブページではしきい値が高すぎるので、ブログ形式をとることにしました。
よろしくお願いします。