2014年7月22日火曜日

WPF用縦書きテキストブロック Tategaki

(2015/12/19追記)
Google等からこのページに直接来られる方が多いようなので。最新版はver.2.1.1になっていますので、こちらからどうぞ。


今回はWPFでの縦書きテキストについてです。

Win32API等を叩いたことがある人に「縦書きのテキストを表示したいときはどうする?」と聞いたら、おそらく即答で「縦書きフォント(通常のフォント名の頭に@を付けたもの)で90°回転させて文字列を描画する」と言うかと思います。LOGFONT構造体に文字列の角度を指定するメンバがあるので、そこに90°を指定して縦書きフォントを選択したうえでTextOut関数などを呼びだせば縦書きのテキストが完成します。

しかし、WPFではそう簡単にいきません。

というのも、なぜか縦書きフォントが封印されています。まあそもそもWPFはGDIベースではないので仕組みが全く違うと言えばそれまでなんですが、フォントファミリ名に@を付けたところで縦書きフォントになってくれません。

困りました。

じゃあどうするか。グリフというものを使えば良いようです。
フォントはあらゆる字に対しての統一的な書体のことを言いますが、グリフっていうのは個々の字そのものの形を意味する言葉らしいです。初めて聞きました。
C#では一般的にUnicodeで文字列が管理されていますが、そのある字に対して、そのUnicodeを「グリフインデックス」と呼ばれるあるフォント特有のインデックスに変換することで、フォントデータからその字を呼び出し画面に表示するという処理をしているようです。そのレベルでテキストをいじるクラスがGlyphsクラスのようです。普段は手を出さなくてもよさそうな低レイヤーなところですね。

当然、縦書きと横書きなんかで字のUnicode値が変わることはありませんが、グリフでは変わるものが出てきます。
例えば、句読点、括弧、記号などです。横書きの文字をそのまま縦に並べると、句読点の打たれる位置に違和感が出るでしょう。それ以外にも、括弧や記号などの形がおかしくなります。










このように書けば一目瞭然ですね。なので、縦書きは縦書き用のグリフに変換してやる必要があります。

一般的にこのUnicode値とグリフインデックスの変換に関しては法則性は無く、テーブルを介して変換するようです。
例えば、C#ではGlyphTypeface.CharacterToGlyphMapというプロパティを使って文字をグリフインデックスに変換してやることができます。しかしこれは横書き用の変換で、縦書き用の変換に関するものはC#には用意されていないようです。

非常に不便ですね。嫌になっちゃいます。なんでWin32APIであんなに簡単にできていたことでC#でこんなに苦労せにゃならんのだと。ついでに言えば、英語圏の人は別に縦書きなんて必要としないので、そういう意味でもこの縦書きに関する記事は非常に少ないです。


さて、愚痴だらけになってしまいましたが、今回の記事の目標はWPFでテキストブロックの縦書き版みたいなものを作るところにしたいと思います。

結論から言いますと、UniscribeというMicrosoftのテキストレンダリングエンジンを使うことで頑張って縦書き用グリフインデックスを取得しちゃえば良いという話です。これに関しては、えムナウ氏のブログに参考になりそうなコードが載っておりました。

WPF の縦書き
縦書きライブラリ

ネイティブの関数を呼び出さなければいけないのでDllImportだの[StructLayout(LayoutKind.Sequential)]だのC#らしくないところで散々苦労しなければならなくなってしまっています。が、えムナウ氏がその辺のラッパーを書いてMITライセンスで配布してくださっていますので、こちらを今回は使わせていただくことにしました。えムナウ氏のコードは縦書き用グリフインデックスを取得するあたりに特化しており、WPFのコントロール部はほとんど入っておりません。なので、今回はこれを実装しました。

割とXAMLはシンプルで、グリフを1つ用意し、縦書き用に90°回転させてやるだけです。

<UserControl x:Class="Tategaki.TategakiText"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" >
    <Grid>
        <Glyphs Name="glyphs1" Fill="Black" UnicodeString=" " IsSideways="true">
            <Glyphs.LayoutTransform>
                <RotateTransform Angle="90"/>
            </Glyphs.LayoutTransform>
        </Glyphs>
    </Grid>
</UserControl>

GlyphsはUnicodeStringとIndicesの両方を空にしては行けないようなので、初期値でスペースを与えています。

コードビハインドでは結構ごちゃごちゃやっています。

/// <summary>
/// TategakiText.xaml の相互作用ロジック
/// </summary>
public partial class TategakiText : UserControl
{
    public TategakiText()
    {
        InitializeComponent();
    }

    static Dictionary<string, string> fontPathDictionary = null;
    static Dictionary<string, string> FontPathDictionary
    {
        get
        {
            if(fontPathDictionary == null)
                fontPathDictionary = SearchFontNamePathPair(new CultureInfo[] { CultureInfo.CurrentCulture, new CultureInfo("en-US") });
            return fontPathDictionary;
        }
    }

    /// <summary>
    /// 有効なフォントの一覧を取得するメソッド
    /// </summary>
    /// <param name="cultures">フォントのカルチャの配列</param>
    /// <returns></returns>
    static Dictionary<string, string> SearchFontNamePathPair(IEnumerable<CultureInfo> cultures)
    {
        Dictionary<string, string> ret = new Dictionary<string, string>();

        string FontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);

        //キーを読み取り専用で開く
        RegistryKey regkey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", false);
        string[] FontFiles = regkey.GetValueNames().Select(p => (string)regkey.GetValue(p)).ToArray();

        foreach(string file in FontFiles) {
            try {
                string path = FontDir + System.IO.Path.DirectorySeparatorChar + file;
                GlyphTypeface typeface = new GlyphTypeface(new Uri(path));

                foreach(CultureInfo culture in cultures) {
                    string FamilyName = typeface.FamilyNames[culture];

                    if(!string.IsNullOrEmpty(FamilyName) && !ret.ContainsKey(FamilyName))
                        ret.Add(FamilyName, path);
                }
            }
            catch(FileFormatException) { }
            catch(NotSupportedException) { }
        }

        return ret;
    }

    /// <summary>
    /// このコントロールで使えるフォント名を列挙するメソッド
    /// </summary>
    /// <returns>使えるフォントファミリ名</returns>
    public static string[] GetAvailableFonts()
    {
        return FontPathDictionary.Keys.ToArray();
    }

    /// <summary>
    /// 表示テキスト
    /// </summary>
    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        "Text", typeof(string), typeof(TategakiText), new PropertyMetadata((d, e) => {
            TategakiText me = (TategakiText)d;
            me.RedrawText();
        }));

    /// <summary>
    /// 表示テキスト
    /// </summary>
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    /// <summary>
    /// 文字間隔
    /// </summary>
    public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register(
        "Spacing", typeof(double), typeof(TategakiText), new PropertyMetadata((double)100, (d, e) => {
            TategakiText me = (TategakiText)d;
            me.RedrawText();
        }));

    /// <summary>
    /// 文字間隔
    /// </summary>
    public double Spacing
    {
        get { return (double)GetValue(SpacingProperty); }
        set { SetValue(SpacingProperty, value); }
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        switch(e.Property.Name) {
            case "FontFamily":
                try {
                    this.glyphs1.FontUri = new Uri(FontPathDictionary[FontFamily.Source]);
                    RedrawText();
                }
                catch(KeyNotFoundException) {
                    throw new ArgumentException("Cannot use this font.");
                }
                break;
            case "FontSize":
                this.glyphs1.FontRenderingEmSize = FontSize;
                RedrawText();
                break;
            case "Foreground":
                this.glyphs1.Fill = Foreground;
                RedrawText();
                break;
        }
        base.OnPropertyChanged(e);
    }

    void RedrawText()
    {
        if(string.IsNullOrEmpty(Text))
            this.glyphs1.UnicodeString = " ";
        else {
            ushort[] glyphs = Uniscribe.GetGlyphs(Text, FontFamily.Source, (int)FontSize);
            string[] IndicesTexts = glyphs.Select((p, i) => {
                StringBuilder sb = new StringBuilder();
                sb.Append(p);

                if(i < glyphs.Length - 1)
                    sb.AppendFormat(",{0}", Spacing);

                return sb.ToString();
            }).ToArray();

            this.glyphs1.UnicodeString = Text;
            this.glyphs1.Indices = string.Join(";", IndicesTexts);
        }
    }
}

まず、フォントファミリ名とフォントファイルの対応付けをやっています。
フォントのディレクトリを取得し、一方、レジストリからインストールされているフォントの一覧を読みだして、その一つ一つについてフォントファミリ名を取得しています。使えないのは省いています。
フォントディレクトリ内のファイルを列挙して総当りする処理もやってみましたが、かなり時間が掛かったので(おそらく例外周りのせい)とりあえずレジストリの方法にしています。

あとは、表示テキストや文字間隔の依存関係プロパティを作り、OnPropertyChangedをオーバーライドして特定のプロパティが変更されたときにグリフの設定を変更する作業などもしています。
RedrawTextメソッドがそのメソッドで、取得したグリフや文字間隔の値を使いながらIndicesをフォーマットし、設定しています。

このコントロールを使用すとこんな表示ができるようになります。


これは、次のようなXAMLで実現しています

<Window x:Class="Frontend.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:Frontend.Views"
        xmlns:vm="clr-namespace:Frontend.ViewModels"
        xmlns:tg="clr-namespace:Tategaki;assembly=Tategaki"
        Title="Tategaki Sample" Height="640" Width="480">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
    
        <!--Viewに特別な要件が存在しない限りは、トリガーやアクションの自作にこだわらず積極的にコードビハインドを使いましょう -->
        <!--Viewのコードビハインドは、基本的にView内で完結するロジックとViewModelからのイベントの受信(専用リスナを使用する)に限るとトラブルが少なくなります -->
        <!--Livet1.1からはコードビハインドでViewModelのイベントを受信するためのWeakEventLisnterサポートが追加されています --> 
        
        <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

        <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->

    </i:Interaction.Triggers>
    
    <StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
        <tg:TategakiText Text="羅生門"
                         FontFamily="メイリオ" FontSize="36" Spacing="200"
                         HorizontalAlignment="Center" VerticalAlignment="Center"
                         RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiText Text="芥川龍之介"
                         FontFamily="メイリオ" FontSize="18" Spacing="100"
                         HorizontalAlignment="Center" VerticalAlignment="Bottom"
                         RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiText Text="すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の"
                         FontFamily="メイリオ" FontSize="18" Spacing="100"
                         HorizontalAlignment="Center" VerticalAlignment="Top"
                         RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <!-- 中略 -->
    </StackPanel>
</Window>

スタイルとか使えばいいじゃんって突っ込まれそうですけど、まあサンプルなのでプロパティの使い方がわかりやすいようにこういう記述のしかたをしています。
このTategakiTextには改行機能は無いので現状1行ずつインスタンスを作っていますが、それを順に並べるのにはStackPanelを使っています。ごく自然なStackPanelの使い方だとは思いますが、右から順に並べたいのでFlowDirectionをRightToLeftに指定しています。しかし、これを指定するとStackPanel内の表示内容も左右反転してしまうようなんですね。アラビア語とかの使用を想定したインターフェースなのでしょうか。とりあえず、左右が逆転していては仕方ないので、各TategakiTextをRenderTransformで左右反転しています。

これでめでたく縦書きをWPFコントロールというコンテナ化された状態で使えるようになりましたとさ。めでたしめでたし。


サンプルコードを含むこのライブラリのダウンロードはこちらからできます。
WPF用縦書きテキストブロック Tategaki ver.1.0.0
((2015/1/22)都合により削除しました。ver1系はver.1.1.2を使ってください)

2014年7月15日火曜日

PIC24FJ64GB002でUSBメモリーにアクセスする

最近、秋月電子でPIC24FJ64GB002の取り扱いが始まりました。
USB-OTGに対応したPICとしてはかなり有名どころで、多くの書籍やウェブページでも取り上げられており、資料は豊富にあります。にもかかわらず入手性は最悪で、国内の通販では共立電子くらいでしか取り扱っていなくて、さもなくばDigiKeyとかで海外から取り寄せるくらいしか方法はありませんでした。
しかし、ついに秋月電子で取り扱いが始まりました。秋葉原に行くだけで買える!しかも、共立電子の半額以下という破格の値段です。

というわけで、早速買ってきました。

 
手前がPIC24FJ64GB002で、奥がPIC32MX250F128Bです。
この2つ、前者は16bitマイコン、 後者は32bitマイコンでアーキテクチャも何もかも全く違うのかなと思ったら、なんとまあピンコンパチ(多分)でした。少なくとも電源やUSB周りは同じで、PIC32MXのほうをいじってみたときに使った回路をそのまま転用することができました。
というわけで、そのPIC32MXをいじったときに最終的にいきついたFatFsを動かすお話に用意したプログラムをPIC24FJ64GB002に移植してみました。


と言ってもほとんど苦労はしません。

そもそもこのデモプログラムはPIC24FJ64GB004でも動くように作られてて、多分これと002の違いはピン数とかパッケージとかその程度です。また、FatFsは完全にデバイス非依存で、型の大きさとかに気をつけておく程度でちゃんと動作はしてくれるはずです。というわけで、プログラムの修正はほとんど必要ありません。


まずは前回のプロジェクトのプロパティからDeviceをPIC24FJ64GB002に変更します。
…と言いたいところですが、PIC32MXのほうの設定を消すのはもったいないです。かと言ってバックアップとってゴニョゴニョやるのもめんどくさい。というわけで、プロジェクトのコンフィグレーションを追加してしまいましょう。MPLAB Xのプロジェクトはコンフィグレーションと呼ばれるものを複数作っておくことで、同じ構成のプログラムを別のデバイスに簡単に移植できるようになっているようです。もちろん、コードがそれに対応していたらの話ですが。

プロパティのカテゴリーの欄の下にある「Manage Configurations...」というボタンを押すとこんな画面が出てきます。


Duplicateで既存のコンフィグレーションをコピーできますが、まあコピーしてもコンパイラが違うとコンパイラの設定が全部吹っ飛んじゃうんであまり意味無いでしょう。ということで、Newを押して、適当な名前のコンフィグレーションを作ります。そして、Set Activeボタンを押せば、そのコンフィグレーションがアクティブになります。


そして、作ったPIC24FJ64GB002のほうのコンフィグレーションに対して、設定をしていきます。デバイスが未設定なので、右上のデバイスの欄からPIC24FJ64GB002を選択します。そして、コンパイラをXC16に設定します。そして前回同様、インクルードファイルのパスとヒープ領域の容量を設定してあげます。


次に、ソースコードの修正を若干します。修正箇所は、コンフィグレーションビットの設定とマイコンの初期化のプログラムのみです。

とは言っても、デモプログラムのほうにPIC24FJ64GB004のコードが入っているので、プリプロセッサの__PIC24FJ64GB004__を__PIC24FJ64GB002__に変更してやるだけです。というより、#if definedで追加してやる形にしました(具体的なコードはMicrochipの著作物なので掲載は遠慮しておきます。各自MLAのサンプルコードからコピーしてください)。

#if defined(__PIC24FJ64GB002__) || defined(__PIC24FJ64GB004__)
    //中略
#elif defined( __PIC32MX__ )
    //中略
#else
    #error Unsupposed Processor
#endif

コンパイルは、クリーンビルドをしてください。PIC32MXのほうの何かが残っているとリンクで謎のエラーが起きて焦ります。

そして、書き込みが終わればUSBメモリーを挿して、めでたくサンプルファイルの書き込みがされて完成です。

めでたしめでたし。



ここまで書いてなんですが、値段で見ると、PIC24FJ64GB002が340円、 PIC32MX250F128Bが360円です。20円差でより高性能なPICが買えるなら、別にPIC32MXのほうでいいんじゃないかなーって気はします。
まあでも最初に言った通り、PIC24のほうは資料の豊富さは格別です。気が向いたらBluetooth Stackとかに手を出してみようかなー。

2014年7月14日月曜日

NTP時計がフリーズするバグの特定

さて、ここのところあまりブログでは話題にしていなかったNTP時計ですが、全く何もやっていたわけではありません。実は、恐怖の「数日~十数日に1回フリーズするバグ」が起きていました。はい、バグの中では最もタチの悪いやつですね…。

このバグの原因がわかるまでは、いろいろな原因を疑っていました。算術エラー割り込みだとか、A/D変換完了待ちループにおける不具合だとか…。
症状はこんな感じでした。
  • 時計のカウントが止まる(時計の値が進まなくなる)
  • 7セグは正常に表示されている(ダイナミック点灯制御は生きている)
ここからわかることは、メインルーチンがどこかで止まっているであろうってことと、PICのクロックが停止したとかそういうハードウェア寄りの問題ではないことです。
NTPサーバーより取得した時刻はタイマ1でカウントしていますが、 「タイマ1の値を取得して時刻に変換して7セグ表示値として設定する」という作業はメインルーチンでやっています。そして、ダイナミック点灯制御は割り込みでやっています。ダイナミック点灯制御が生きていて時刻が進まないということは、割り込み自体は起こり続けているということなので、CPUが止まっているわけではありませんが、何かしらの理由でメインルーチンが動いていないということです。そのため、何かソフトウェアに由来する問題であろうことが予想されます。

頻度ですが、割とすぐに起こるようなものではなく、止まった日時は
6/26 18:30頃
7/1 7:00頃
7/4 10:00頃
7/14 6:00頃
でした。一部記録が曖昧ですが、まあおおまかな日時はこれであっているはずです。フリーズするバグだけどもダイナミック点灯制御は生きているバグだったので、止まった時刻が容易にわかるのがせめてもの救いです。
しかし、これだけ間隔があいていると、なかなかデバッグも容易ではないですね…。


まずは、算術演算エラーを疑いました。
いわゆる、ゼロ除算エラーです。PIC24Fシリーズはゼロで除算すると無限ループにはまったりするわけでもなく、ちゃんとエラーとして割り込みを起こしてくれるようです。
この割り込みのハンドラは作っていなかったので、何かの拍子にゼロ除算が起こって割り込みが発生した上で、 ハンドラで割り込みフラグをリセットしていないから割り込みから抜け出せず、しかし、PIC24Fは多重割り込みが受け付けられるので、ダイナミック点灯制御の割り込みが算術演算エラーの上で割り込まれて動作していると考えました。
しかし、この予想には、1つ重大な間違いがありました。算術演算エラー割り込みは、割り込みの優先度が11に設定されています(ユーザー変更不可)。しかし、周辺モジュールや外部割込みの優先度は0~7にしか設定できず、すなわち、どんな周辺モジュール割り込みよりも先に算術演算エラー割り込みは起こり、算術演算エラー割り込み中に周辺モジュールが割り込みを起こすことはありえません。
すなわち、この予想は間違っていると言えます。

次に、A/D変換の終了待ちの問題を疑いました。
A/D変換は時間がかかるので、通常、開始の指示を出してからしばらくして終了しているかを確認し、値を読み出します。その終了の認識の方法は、無限ループでフラグをポーリングし続けて確認するだとか、割り込みで確認するだとか、いくらかあります。今回、NTP時計では無限ループの方法を使用していました。

AD1CON1bits.SAMP = 1;
while(!AD1CON1bits.DONE);

これ自体は何もおかしくないコードです。実際、MicrochipのA/D変換のドキュメンテーションにもこのように無限ループでポーリングし続けて変換完了を待つサンプルプログラムが書いてあります。
もしも、これが何かしらの問題で変換が終わってもAD1CON1bits.DONEが1になってくれなかったら、ここでプログラムが止まってしまいます。 要するにハードのバグを疑いました。しかし、そんな記事ググっても無ければ、実際にここで止まっている保証もありません。しかし、原因がわからないと疑心暗鬼になってしまって、特にこういう条件次第では無限ループになりそうなところを疑ってみたくなってしまうんですね…。


フリーズの原因を特定するために、いわゆるprintfデバッグと呼ばれるものの類のコードを実装してみました。
液晶は電源さえ入っていれば表示内容を保持するので、マイコンが何かしらの原因で止まっても、液晶は表示を保ってくれます。なので、特定のコードを通過したときに特定の何かしらの文字列を表示させるプログラムを実行しておけば、何が表示されているかでフリーズ箇所を絞れるというものです。

実際には文字列を出力したわけではなく、点を液晶に打ちました。実際に時計として使いながらのデバッグなので、あまり表示内容を乱したくないですしね。
最初はある一定の処理を終えるごとに点を1つずつずらしていくプログラムにしましたが、いざフリーズしてみると、確かに点が表示されているのはわかるものの、それが何段目のピクセルなのか判別がとてもじゃないですがつきません。それくらい予想してプログラム書けばよかった…。

というわけで、次は線にして、線の長さを順に伸ばしていくようにしました。それなら、一生懸命ドット数を数えればエラー箇所がわかります。
実際は、小型液晶ゆえにドットの個数を肉眼で数えるのは厳しいです。なので、フリーズしたら写真で撮って拡大して見ました。

はい、その写真がこちらになります。


画面の右下にドットを打ってありますが、6つのドットが表示されていることがわかります。

で、6ドット分の線が表示されているときに実行中のプログラムはどこかなと思ってソースコードを見てみたら、A/D変換を終えて7セグの明るさの設定値をセットする関数でした。A/D変換の完了待ちではありません。
で、問題のコードがどうなっていたかというと、こうなっていました。

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;
}

この中で疑わしいところを探します。
掛け算とか割り算とかキャストとかは実際XC16コンパイラがどんなふうに展開しているかが見えないところなのでそういうところについつい目が行ってしまいますが、そこは信用するとした場合、もう残っているのはPWMOnStart変数等に代入しているところくらいしかありません。

( ˘⊖˘) 。o(待てよ…)

このPWMOnStartやPWMOffStartという変数は、7セグのダイナミック点灯用の割り込み中にも呼び出されるものです。
こういう、メインルーチンと割り込みルーチンの両方から参照される変数には細心の注意を払う必要があります。過去から現在にわたってプログラマーを苦しめ続けているマルチスレッドのトラウマ的存在としてとても有名なものです。

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;
    }
}

こうやって割り込み中に7セグのON時間やOFF時間を制御するのに使っています。
PWMOnStartとPWMOffStartは0~PWM_PERIODの値になり、TMR2はPWM_PERIODになると割り込みが入るようにコンパレーターをセットしています。PWMOnStartやPWMOffStartが極端にPWM_PERIODに近かった場合、割り込み処理中に次の割り込みが入ってしまい、すなわち、_T2Interrupt関数を抜けた瞬間に再び_T2Interrupt関数に入ってしまいますが、PWMOnStartとPWMOffStartは足してPWM_PERIODになるようにしてありますので、片方が極端にPWM_PERIODに近くてももう片方は近くなくなるので、そっち側でメインルーチンにCPUを割くことができます。

はい、上の表現、めちゃくちゃ強調している意味、わかるひとにはわかりますね。

本当に、常に足してPWM_PERIODになるの?


こういう複数のスレッドにまたがって使われる変数は、一連の変数書き換えシーケンス中に割り込みが起こった場合を想定する必要があります。

もうお分かりでしょう。
LED7SEG_SetBrightness関数の5行目、PWMOnStartの値が代入されてから、次の行でPWMOffStartの値が代入されるまでの間にタイマ2の割り込みが入ったらどうなるでしょう?
しかも、もともとはPWMOffStartが極端にPWM_PERIODに近い値で、 次にPWMOnStartを極端にPWM_PERIODに近い値にしようとしたタイミングでの割り込みだったらどうなるでしょう?

両方とも、極端にPWM_PERIODに近い値で割り込みが入っちゃうんです。

そうなった場合、もうおしまいです。7セグのON表示処理をし終わったら直ちにOFF表示処理がされ、それが終わったら直ちにON表示処理を…といった状態になってしまい、二度と割り込みルーチンから抜けることがなくなってしまいます

これが今回のバグの原因だったんですね。


さて、ここまででこれがバグの原因だったということで片がつきました。しかしそれは情況証拠であって検証したわけではありません。そもそも、数日経ってやっと発動するかどうかのバグなのに、安定動作してると主張するには一体何日待てば良いのよって話です。
しかし、ラッキーなことに、この変数書き換え中の割り込みがフリーズの原因だった場合、フリーズの状態からそれを裏付けることができます。

それは、7セグの明るさです。

バグがあったとされたのは7セグのON処理時とOFF処理時の時間の長さを変えることで7セグの明るさを調節するプログラムですが、このバグが発動して割り込みルーチンから抜けられていないときは、ON処理とOFF処理の時間がともに最小で終わってしまっているということです。すなわち、デューティー比はおよそ1:1、少なくとも「明るいモード」や「暗いモード」の間の明るさに見えるはずです。

はい、実際に見比べてみましょう。
そのためには、外が真っ暗な時間に部屋の蛍光灯を付けて、同じ位置から同じ露出設定で写真を撮ってみれば、写真として客観的に明るさを見られますね。


上からフリーズ状態、輝度最高、輝度最低のモードです。
本当は三脚使って完全に定点撮影にすればよかったんでしょうが、面倒だったので同じ位置に立って手持ちで撮影しました。

もう一目瞭然ですね。写真でも明るさの違いがよくわかります。

というわけで、原因はこれで確定ってことで良いでしょう。

対策は簡単です。PWMOnStartとPWMOffStartの書き換え中は割り込みを禁止してやればいいだけです。

void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;

    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);

    LED7SEG_DisableInterrupt();

    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;

    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);

    Brightness = value;

    LED7SEG_EnableInterrupt();
}

はい、これでPICを書き換えて完成です。
このように、途中でスレッドが切り替わってはいけない部分のことを、プログラミング用語で「クリティカルセクション」と言います。C言語にはそのような機能はありませんが、言語によってはそれを実現するための機能があるものもあります。
マルチスレッドでは、片っ端から「ここでもし割り込みが起こったらどうなる?」ということを念頭に置いてプログラムを書かなければなりません。すごく大変なことですが、それを怠ると今回みたいなバグを誘発してしまいますし、何しろこの手のバグは再現性がなかなか無いので原因を探るだけでも一苦労です。こういうバグを出さない、もしくは出してもこれが原因だと断定できるようになるまでは結構なプログラミングの習熟度が必要かと思いますが、まあ、この記事を読んでくださった皆さんももしこのような再現性の低いバグに出会った時は、この記事のことを思い出してくれればなと思います。


これで安定して動作してくれ~~~~~~

2014年7月12日土曜日

WindowChromeでWPFのRibbonWindowの枠を完璧にする

前回の記事の続きになります。
もはや永遠のテーマとも言えた.NET4.5のRibbonWindowの枠の話です。
なんとか

解 決 出 来 ま し た \(^o^)/

(´◔౪◔)۶ヨッシャ!

さて、それでは完成図をご覧いただきましょう。


前回の記事で1つ目の問題になっていた、ウィンドウの外枠とクライアント領域の間の境界線がちゃんと引けています。実はこれ、他愛のないことだったんですけどね。
ですが、何度も苦労させてきた最大化するとタイトルバーが黒くなる問題、これもバッチリ解決出来ました。


バッチリです。

とりあえず、例によってXAMLを載せます。

<RibbonWindow x:Class="WindowChromeTest.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:WindowChromeTest.Views"
        xmlns:vm="clr-namespace:WindowChromeTest.ViewModels"
        Title="MainWindow" Height="350" Width="525" BorderThickness="5" >

    <WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="7,29,7,7" NonClientFrameEdges="Bottom" ResizeBorderThickness="7" />
    </WindowChrome.WindowChrome>
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

    </i:Interaction.Triggers>

    <DockPanel>
        <Border BorderBrush="Black" />
        <Ribbon DockPanel.Dock="Top" BorderThickness="1" >    
            <Ribbon.QuickAccessToolBar>
                <RibbonQuickAccessToolBar>
                    <RibbonButton SmallImageSource="/WindowChromeTest;component/Images/Configuration.ico" />
                    <RibbonButton SmallImageSource="/WindowChromeTest;component/Images/Information.ico" />
                </RibbonQuickAccessToolBar>
            </Ribbon.QuickAccessToolBar>
            <RibbonTab Header="Home">
                <RibbonGroup Header="Group">
                    <RibbonButton LargeImageSource="/WindowChromeTest;component/Images/Configuration.ico" Label="Config" />
                </RibbonGroup>
            </RibbonTab>
        </Ribbon>
        <TextBox Text="これはテキストボックスです" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Visible" />
    </DockPanel>
</RibbonWindow>

まず、第1の問題点の境界線の話ですが、これは、グラスフレームの範囲を具体的な数字で指定することで解決しています。
要するに、 全体をグラスフレームにしちゃうと当然その中に描いたものとグラスフレームの境界線は引かれなくなってしまいますが、ちゃんとクライアント領域の外側だけをグラスフレームにすると、そことクライアント領域の中の間の境界線が引かれるということです。

第2の問題点の最大化ですが、これはなぜ解決できたのか実はよくわかっていません。
しかし、NonClientFrameEdgesプロパティをBottomにしたら解決できてしまいました。

WindowChrome.NonClientFrameEdges プロパティ

「ウィンドウ フレームのどの外枠がクライアントによって所有されないかを示す値」って一体どういう意味だ?
意味わかりません。機械翻訳だから意味わからないのかなと思って原文を見てみましたが、原文もほとんどそのままで、結局意味はわからずじまいです。そもそも、ウィンドウフレームがクライアントに所有されたらなぜ最大化したときに黒くなくなるのかっていうのがわかっていませんからね…。

ちなみに、挙動をいろいろ研究してみたところ、
  • Topにする→右上の閉じる、最大化、最小化のボタンが押せなくなった
  • Rightにする→右上の閉じる、最大化、最小化のボタンが若干右に寄って見栄えが悪くなった
という現象が起こりました。Leftにしたらソフトウェアのアイコンが左に寄って見栄えが悪くなるのかなーって思ったらどうもそうではない模様。ほんとよくわからないです。

どうもこのプロパティ、.NET4.5から実装されたプロパティみたいで(ていうか、WindowChrome自体が参照を追加せずとも使えるように組み込まれたのが.NET4.5から)、資料も全然出回っていないようなんですよね。
何か詳しいことを知っている人がいたらぜひ教えてください。

WindowChromeでWPFのRibbonWindowの枠を改善する

以前、こちらの記事で.NET4.5ではリボンウィンドウの枠が残念な感じになってしまうという話をしました。しかし、WindowChromeをいじるといいみたいな話がどこかで出ていたので、ちょっと色々と試してみました。

WindowChromeクラスはウィンドウの非クライアント領域のカスタマイズをするためのクラスのようです。

WindowChrome クラス

私も詳しくはわかっていないのですが、このクラスをうまく使うと、Windowsの一般的なウィンドウ枠を一切使わない独自のウィンドウ枠や、もしくはWindows7のWindowsフォトビュアーやエクスプローラーなどで見られる、画面全体にAeroのすりガラスのエフェクトを掛けたりするといったことができるようです。

それでは一度ここで、WindowChromeを使ってない、以前の記事にも出てきたみすぼらしいウィンドウ枠を見ておきましょう。


このようにウィンドウ枠が細り、角もきれいな円弧となっていません。

ここで、WindowChromeを使って全体をすりガラスにし、さらにボーダーの太さをいじってみました。


おっ!これはいい感じだ!だいぶマシになってる!
ではここでソースコード行ってみましょう。

<RibbonWindow x:Class="WindowChromeTest.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:WindowChromeTest.Views"
        xmlns:vm="clr-namespace:WindowChromeTest.ViewModels"
        Title="MainWindow" Height="350" Width="525" BorderThickness="5" >

    <WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1" ResizeBorderThickness="7" />
    </WindowChrome.WindowChrome>

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

    </i:Interaction.Triggers>

    <DockPanel>
        <Border BorderBrush="Black" />
        <Ribbon DockPanel.Dock="Top" BorderThickness="1" >    
            <Ribbon.QuickAccessToolBar>
                <RibbonQuickAccessToolBar>
                    <RibbonButton SmallImageSource="/WindowChromeTest;component/Images/Configuration.ico" />
                    <RibbonButton SmallImageSource="/WindowChromeTest;component/Images/Information.ico" />
                </RibbonQuickAccessToolBar>
            </Ribbon.QuickAccessToolBar>
            <RibbonTab Header="Home">
                <RibbonGroup Header="Group">
                    <RibbonButton LargeImageSource="/WindowChromeTest;component/Images/Configuration.ico" Label="Config" />
                </RibbonGroup>
            </RibbonTab>
        </Ribbon>
        <TextBox Text="これはテキストボックスです" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Visible" />
    </DockPanel>
</RibbonWindow>

WindowChromeでリサイズボーダーの太さを設定すると同時に、RibbonWindowのほうでボーダー太さも設定していることに注意してください。

ただし、これが良いことずくめかというとそういうわけでは無いようです。

まず1つ目は気づいている方もいるかとは思いますが、クライアント領域の外側の枠がありません。なんかのっぺらぼうみたいな感じで違和感ありますね…。なんかいい方法は無いんでしょうかね。

2つ目は割と致命的です。ウィンドウを最大化してみると…


タイトルバーが真っ黒になってしまいましたorz
クイックアクセスツールバーや、最小化、元に戻す、閉じるボタンは有効なようです(この3つのボタンは見えませんがその位置をクリックするとちゃんとその効果があるようです)。

うーん、困ったなあ。Fluent Ribbon Control Suiteも含めて文句なしの手段が無いのがまた困りものです。

ついでにですが、Windows8だと結構まともに動いてくれるようです。



っていうか、そもそもWindows8だとあの極細のみっともないウィンドウ枠もあんまり目立ってダサくないんですよね…。

2014年7月9日水曜日

突然の死プラグイン

_人人人人人_
> 突然の死 <
 ̄Y^Y^Y^Y^Y ̄

とかもうあまり流行らないですかね。
でも、割と私はまだTwitterで使っています(

はい、でもこれ、案外作るのめんどくさいんですよね。
一応辞書登録はしていても、複数行は登録できませんし、そもそも、中に入れる文字列を変更したら人の数やY^の数も変わってきますからね。一発で囲ってくれたらとても楽ですよね。

Androidには良いアプリがあります。Cerisierというアプリです。

Cerisier - Google Play の Android アプリ

このアプリはなんか中段の紙飛行機マーク(ツイートボタン)の左にある、それっぽいやつを押すと自動的にこうやって人とY^で囲ってくれるのです。なにか強調したいときにとても役に立ちますね。


これを、Janetterプラグインとして実装してしまおうというのが今回のお話です。

国産Twitterクライアント「Janetter」

最も有名なTwitterクライアントの1つで、そのユーザー数も非常に多いです。
しかし、いわゆるTwitterのクライアント締め出し政策の一環で設定されたユーザー数上限に達してしまい、すでに新規ユーザーが登録するのは難しい状態になっています。
また、そのような状態になっているためか、事実上開発が終了したような形になっており、ここ暫くの間一切のアップデートがされておりません。

ですが、私のTwitterライフはJanetter無しには語れないというほど、ずっとJanetterでTwitterをしてきた身です。これを手放すわけにはなかなか行かず、未だに使っております。

はい、そしてこのソフト、どうもChromeとJavaScriptベースでできているみたいなんですね。
詳しくは私もわかっていないのですが、そのため、JavaScriptのプラグインを動かす機能があります。

しかし、プラグイン作製のチュートリアルがあるわけでもなく、ちょっとしたWikiがある程度でChromeやJavaScriptの開発経験が皆無な私にとってはまさに手探り状態。ですが、見よう見まねでなんとか作り上げました。

(function(jn){
    $('.tweet-button')    
        .before(
            $('<a class="button" id="totsuzen_no_shi" style="float:right; width: 70px; height:17px; font-weight: bold; line-height:1.5em; margin: 3px 0 2px 3px;">突然の死</a>')
        );
    $('#totsuzen_no_shi').click(function(){
        var lines = String($('#tweet-edit-container > .expanded textarea').val()).trim2().split('\n');
        var lengths = [];    //各行の文字列の長さ
        for(var i = 0; i < lines.length; i++) {
            var length = 0;
            for(var j = 0; j < lines[i].length; j++) {
                var c = lines[i].charAt(j).charCodeAt(0);
                
                if((c >= 0x0 && c < 0x81) || (c == 0xf8f0) || (c >= 0xff61 && c < 0xffa0) || (c >= 0xf8f1 && c < 0xf8f4))
                    length += 0.5;    //半角だ
                else
                    length += 1;
            }
            lengths.push(length);
        }

        var maxlength = Math.max.apply(null, lengths);    //行の長さの最大値

        var totsuzen = '_';
        for(var i = 0; i < maxlength + 2; i++)
            totsuzen += '人';
        totsuzen += '_\n';

        for(var i = 0; i < lines.length; i++) {
            totsuzen += '> ' + lines[i];
            for(var j = 0; j < maxlength - lengths[i]; j++)
                totsuzen += ' ';
            totsuzen += ' <\n';
        }

        totsuzen += ' ̄Y';
        for(var i = 0; i < ((maxlength + 2) * 1.4 - 1) / 2; i++)
            totsuzen += '^Y';
        totsuzen += ' ̄';

        jn.editor._normalTweetBox(totsuzen, false);

        return false;
    });
})(janet);

totsuzen_no_shiというidのボタンを定義し、そのクリック時のイベントを書く形で作っています。
JavaScriptはほとんど開発の経験が無いので、頭が良いコードを書くより動くコードを書くことを前提に作っています。

まずは、冒頭でツイート欄の内容を取得し、それを改行コードでsplitして行ごとに配列としています。
そして、その各行の文字数を取得していますが、ここで、文字コードを判別して全角文字は1文字、半角文字は0.5文字としています。そもそもJanetterのフォントは等幅フォントではないのでこういった書き方はあまりよろしくないのかもしれませんが、実際に表示される文字幅を取得する方法がわからないので仕方ないです。

続いて、その各行の文字数の最大値に合わせて人やY^を並べていっています。それだけです。

最後は_normalTweetBox関数を呼び出して表示内容を更新しています。
なんていうか、先頭にアンダーバーを付けた関数名なあたり、プライベートにしたい感がめっちゃある関数ですが、仕方ない、これを使うしかないので使っています。
他にshowTweetBoxという関数がありますが、これはすでにツイート欄に文字列があった場合、下書きに保存するか確認するダイアログを出してくるので鬱陶しいです。中の文字列に文字を付け足してるだけなのにわざわざ下書きに保存する奴なんていないわ!というわけで、この関数は使うのをやめました。

はい、そんな感じでとりあえず形になっています。

Janetterでこのプラグインを使う場合は、このコードをコピペして適当なファイル(例えばtotsuzen_no_shi.jsなど)にUTF-8として保存し、C:\Program Files (x86)などのフォルダの中にあるJanetter2\Theme\Common\js\pluginsフォルダに突っ込んでJanetterを再起動したらおkです。


こんな風にツイート欄に突然の死ボタンが現れ、それを押すたびにツイート欄の内容をどんどん人とY^で囲っていってくれます。(クソネミキャッスルボタンはまた別のプラグインです)

ぜひJanetterユーザーはお試しあれ。

2014年7月1日火曜日

I2C小型液晶を動かす

最近、秋葉原のaitendoが閉店セールか何かで50%OFFセールをしています。なんか店舗を移転するそうですね。
そこで、16X2-SPLC792-I2CというI2C接続の液晶が売っていました。無論、液晶モジュール単体だけでは使い勝手が悪いので、変換基板セットで買いました。
それにしても、変換基板セットで税抜き375円。だいぶ安いです。3枚以上買おうとすると通常価格になるようなので、1枚当たりの価格が一気に跳ね上がるという"逆数割"ですが、まあ個人で使う分にはなかなかそんなに大量に買わないので問題ないでしょう。

 

買ってきたものがこんなかんじです。部品は全部自分で実装しますが、チップ部品も比較的大きめのものを採用してくれているので、配線はし易いと思います。 ただ、液晶の裏表は間違えないように気をつけてください。バックライトモジュールに上手くはまる方向を意識しながら組み立てれば大丈夫です。


組み上げるとこんなかんじになります。インターフェースにはピンヘッダを取り付けておきました。

ところで、この液晶モジュールにはSPLC792Aという液晶コントローラーが使われているようですが、どうも秋月で売っているI2C小型液晶AQM0802Aのコントローラーと互換性があるらしいです。実はこのAQM0802A、NTP時計を作る前に作ったプロトタイプで使っていたんですね。面白いことに、このaitendoの液晶変換基板のピンアサインまで秋月の液晶のピンアサインと同じになっています。どちらが先か知りませんが、互換性を意識して作ったんですかねw

というわけで、プロトタイプの基板に差し替えて使ってみましたが、上手く動きません…

I2Cスレーブアドレスが違うんじゃないかとか思ってデータシートを見たところ、コントローラーのピンを2つ分使って2bit分のアドレスを自由にカスタマイズできるとかいてありましたが、コントローラーのピンが表に出ているわけもなく、結局4通り手当たり次第にやってみることにしましたが、やはり動きません。

そしていろいろとコードを眺めていたが末にわかったのですが、一連のI2Cのシーケンスを送った後に27usのディレイを、コマンドの方には入れていたのですが、データの方には入れていませんでした。秋月の液晶のほうはそれで動いてくれていたんですが、それを入れている記事等が見つかったので、実際に入れてみました。

見事動きました。秋月の液晶のほうが、コントローラーチップの処理速度が速いってことなんですかね。まあ、なんて言うか、規格外の動かしかたを今までしていたようです。データシートをよく読んでなかったからこんなことろで苦労してしまったんですね(汗

結果、プログラムはこんなかんじになっています。

#include <p24FJ64GA002.h>
#include <stdint.h>
#include "i2c_lcd.h"
#define FCY 16000000UL  //32MHz/2
#include <libpic30.h>

#define LCD_SLAVE_ADDR  (0x3E << 1)

void i2cl_WaitForBusy(void)
{
    while((I2C1CON & 0b0000000000011111) | I2C1STATbits.TRSTAT);
}

void i2cl_Start()
{
    i2cl_WaitForBusy();
    I2C1CONbits.SEN = 1;
}

void i2cl_Stop()
{
    i2cl_WaitForBusy();
    I2C1CONbits.PEN = 1;
}

uint8_t i2cl_Write(uint8_t data)
{
    i2cl_WaitForBusy();
    I2C1TRN = data;
    i2cl_WaitForBusy();

    return I2C1STATbits.ACKSTAT;
}


void i2cl_SendCmd(uint8_t cmd)
{
    i2cl_Start();
    i2cl_Write(LCD_SLAVE_ADDR);
    i2cl_Write(0x00);
    i2cl_Write(cmd);
    i2cl_Stop();
    __delay_us(27);
}

void i2cl_SendData(uint8_t data)
{
    i2cl_Start();
    i2cl_Write(LCD_SLAVE_ADDR);
    i2cl_Write(0x40);
    i2cl_Write(data);
    i2cl_Stop();
    __delay_us(27);
}

void i2c_Init()
{
    TRISB |= 0x0300;
    I2C1BRG = 0x0013;
    I2C1CON = 0x8000;
}

void i2cl_Init()
{
    i2c_Init();
    __delay_ms(40);
    i2cl_SendCmd(0x38);
    i2cl_SendCmd(0x39);
    i2cl_SendCmd(0x14);
    i2cl_SendCmd(0x70);
    i2cl_SendCmd(0x56);
    i2cl_SendCmd(0x6C);
    i2cl_SendCmd(0x38);
    i2cl_SendCmd(0x0C);
    i2cl_SendCmd(0x01);
    __delay_ms(1);
}

void i2cl_Write1stLine(char *text)
{
    i2cl_SendCmd(0x03);
    while(*text)
        i2cl_SendData(*text++);
}

void i2cl_Write2ndLine(char *text)
{
    i2cl_SendCmd(0xC0);
    while(*text)
        i2cl_SendData(*text++);
}

あ、プロセッサはPIC24FJ64GA002です。NTP時計は表面実装にしたかったので004を採用しましたが、この2つはアーキテクチャが非常に似ているのでかなりプログラムが移植しやすいです。

これで、見事ソフトウェアの変更なしで液晶を差し替えるだけで動いてくれるようになりました。



ソフトウェアの変更なしで差し替えるだけで動くってことはI2Cスレーブアドレスも同じなんですよね…。なんていうか、せっかくなら同じI2Cバスに並べて動かしたかったと思うと、うーん、違ってもよかったような…。