2024年4月13日土曜日

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

 最近Tategaki熱が再燃しています。大幅に機能を追加しました。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

追加した機能

TategakiTextのプロパティ

追加した機能はReadmeのほうにも書いてありますが、TextBlockにある主要なプロパティを TategakiTextにも追加し実装しました。追加したプロパティは以下の通りです。

  1. TextWrapping
  2. TextDecorations
  3. LineHeight
  4. TextAlignment
  5. Padding
  6. LastForbiddenChars / HeadForbiddenChars / LastHangingChars
  7. EnableHalfWidthCharVertical

1番から5番まではTextBlockにもあるプロパティで、その挙動も極力TextBlockに似せているので特段使い方の説明はいらないと思います。

TextWrappingが実装されたことにより、今まで1行表示しかできなかったTategakiTextが複数行表示もできるようになりました。改行文字も認識しますので、下のデモソフトのように小説のような長い文章でもちゃんと折り返して正確に表示できます。TategakiMultilineは多数のTategakiTextをItemsPanelで並べて折り返しを実現していたので、それに比べればだいぶ動作が軽快になりました。

禁則文字 / ぶらさげ文字

TategakiMultilineには禁則文字(文末禁止文字 / 文頭禁止文字)が設定できました。TategakiTextでも同様の設定ができるようになったうえ、文末ぶらさげ文字も設定できるようになっています。文末に来た場合、はみ出し前提で下にぶらさげる文字ですね。

また、TextWrapping列挙型にはWrapとWrapOverflowという2種類のオプションがあります。

TextWrapping.Wrap


TextWrapping.WrapOverflow

長い一単語があって幅が入りきらなくなったとき、単語の途中で折り返すのがWrap、単語を右側にはみ出させるのがWrapOverflowです。もちろんTategakiTextもこの機能に対応しています。禁則文字の処理の一環として実装されています。

EnableHalfWidthCharVertical

半角の文字を縦書きにするかどうかのオプションです。

上に2つのテキストがありますが、左がこのオプションがOFF、右がONです。ちなみにフォントにvrt2が含まれている場合はこのオプションにかかわらず左側のスタイルになります。

フォント読み込み処理回り

従来はEnvironment.GetFolderPathメソッドを使ってシステムのフォントファイルを読み込んで縦書きが有効なフォントを抽出していました。

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

var uris = Directory.GetFiles(FontDir, "*.ttf").Concat(Directory.GetFiles(FontDir, "*.otf")).Select(p => new Uri(p))
    .Concat(Directory.GetFiles(FontDir, "*.ttc").SelectMany(p => {
        using(var fs = new FileStream(p, FileMode.Open, FileAccess.Read)) {
            return Enumerable.Range(0, TypefaceInfo.GetCollectionCount(fs)).Select(i => new UriBuilder("file", "", -1, p, "#" + i).Uri);
        }
    })
);

これはグリフレベルで描画をする際にどうしてもフォントのURIが必要だからです。ですが、いわゆる「游ゴシック」などのフォントファミリー名からURIを取得する手法がわからず、逆にURIからフォントファミリー名を取得してテーブルとして保持していました。

しかし、フォントはWindowsの特定のユーザーのみにインストールすることもでき、その場合はシステムフォルダ(C:\Windows\Fonts;GetFolderPathで取得できるフォルダ)にフォントファイルは入りません。ですので今までそのようなフォントは読み込めませんでした。

ユーザー用フォントのフォルダを足すのは簡単ですが、Windowsの仕様変更があればまたソフト側でも対応する必要が出てきます。ですので、やはり何かOSやフレームワークが提供する何らかの方法でフォントを取得したいですよね。

そしていろいろと調べていった結果、最終的に以下のような処理にたどり着きました。

var fonttable = new Dictionary<string, VerticalFontInfo>();
var namelist = new List<string>();

foreach(var ff in Fonts.SystemFontFamilies) {
    var tf = new Typeface(ff.Source);
    if(!tf.TryGetGlyphTypeface(out var gtf))    // GlyphTypefaceが取得できなければ用無し
        continue;

    int num = gtf.FontUri.Fragment == "" ? 0 : int.Parse(gtf.FontUri.Fragment.Replace("#", ""));
    var tfi = new TypefaceInfo(gtf.GetFontStream(), num);

    VerticalConverterType convtype = VerticalConverterType.None;
    if(tfi.GetVerticalGlyphConverter().Count > 0)
        convtype |= VerticalConverterType.Normal;
    if(tfi.GetAdvancedVerticalGlyphConverter().Count > 0)
        convtype |= VerticalConverterType.Advanced;
    if(convtype == VerticalConverterType.None)    // 縦書きコンバーターが取得できなければ用無し
        continue;

    var vfi = new VerticalFontInfo(gtf, ff.Source, convtype);
    namelist.Add(vfi.OutstandingFamilyName);
    foreach(var name in vfi.FamilierFamilyNames.Select(p => p.familyname).Distinct())
        fonttable[name] = vfi;
}

namelist.Sort();

まず、Fonts.SystemFontFamiliesでシステムに存在するフォントファミリー名を取得します。その後、そのフォントファミリー名をもとにTypefaceクラスをインスタンス化し、そこからGlyphTypefaceを取得することで、そのメンバからURIを取得することができるのです。

ただ、Fonts.SystemFontFamiliesで取得したフォントファミリー名とGlyphTypeface内にあるフォントファミリー名が違うことがあるようです。この辺のWindowsの挙動がほんとよくわからないですが、Tategakiではどのファミリー名でも目的のURIにたどり着けるように一生懸命キャッシュしています。

 

さて、ここまで作ったら、まあだいたいの用途は満たせそうなのでこんなもんで良いかな…。 TextTrimmingの実装とかをやっても良いかもしれませんが、本家TextBlockって実はHTMLみたいな結構強力な表現ができるみたいで、いろいろ実装していたらきりが無いですからね。まあ、複数フォントを混ぜたかったら複数TategakiTextを並べれば良いだけですし。

2024年4月6日土曜日

WPFでFrameworkElementを直接継承したコントロールを作成する

WPFでコントロールを自作することはさほど多くありません。標準コントロールのほかExtended WPF Toolkitなどのライブラリが充実していることに加え、テンプレートやスタイル、その他もろもろの強力な機能により自作コントロールが無くてもかなりの表現ができてしまうからです。
それでももし何かコントロールが必要になったらカスタムコントロールでいくつかのコントロールを集めたコントロールを作ることができます。これでだいたい事足りてしまうのです。

それでも、もっと原始的なコントロールを作りたいことがあったらどうすれば良いでしょう。そんなことは普通は無いと思っていたのですが、WPF縦書きライブラリを作る過程で必要になってしまったので、そのようなときの手段を今回はまとめておきたいと思います。

 

WPFのレイアウトプロセス

まず最初に理解しなければならないのは、WPFにおけるコントロールの配置の仕組みです。多くの人は、Grid / StackPanel / Canvasの3種類のパネルで、コントロールに与えるパラメーターは同じなのにコントロールのサイズなどの配置のされ方が全く違うことを経験したことがあるでしょう。

Grid

<Grid ShowGridLines="True">
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="1*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="Red" Foreground="White" Background="Red" />
    <TextBlock Grid.Row="0" Grid.Column="1" Text="Yellow" Foreground="Black" Background="Yellow" />
    <TextBlock Grid.Row="1" Grid.Column="0" Text="Red" Foreground="White" Background="Blue" />
    <TextBlock Grid.Row="1" Grid.Column="1" Text="Green" Foreground="White" Background="Green" />
</Grid>

StackPanel

<StackPanel Orientation="Vertical">
    <TextBlock Text="Red" Foreground="White" Background="Red" />
    <TextBlock Text="Yellow" Foreground="Black" Background="Yellow" />
    <TextBlock Text="Red" Foreground="White" Background="Blue" />
    <TextBlock Text="Green" Foreground="White" Background="Green" />
</StackPanel>

Canvas

<Canvas>
    <TextBlock Canvas.Left="50" Canvas.Top="50" Text="Red" Foreground="White" Background="Red" />
    <TextBlock Canvas.Left="100" Canvas.Top="50" Text="Yellow" Foreground="Black" Background="Yellow" />
    <TextBlock Canvas.Left="50" Canvas.Top="100" Text="Red" Foreground="White" Background="Blue" />
    <TextBlock Canvas.Left="100" Canvas.Top="100" Text="Green" Foreground="White" Background="Green" />
</Canvas>

それぞれのパネルに4つのTextBlockを配置してみました。一部パネル内のどこに配置するかの添付プロパティは追加していますが、それ以外はどのパネルに対しても同じパラメーターでTextBlockを配置しています。ですが、Gridの場合は右/下方向に引き伸ばされ、StackPanelは右方向のみに引き伸ばされ、Canvasは一切引き伸ばされていません。TextBlockからはパネルのどの位置に配置するのかしか指定しておらず、寸法はパネルの種類によって自動で決まるのです。それ以外にもHorizontalAlignmentプロパティVerticalAlignmentプロパティも配置に影響するのはご存じのとおりです。
これは一体どうやって実現されているのでしょうか。

MeasureOverrideとArrangeOverride

WPFのレイアウトは、Measure(測量)とArrange(配置)という2つのプロセスを経て決定されます。

1. Meausre

親要素が子要素の配置を考えるうえで、子要素に必要な大きさを申告してもらうための手続きです。 

親要素はMeasureが必要になった時に子要素のMeasureメソッドを呼びます。そうすると子要素はDesiredSizeプロパティを更新します。FrameworkElementを継承したクラスを実装するうえでは、MeasureOverrideメソッドをオーバーライドすることでその手続きを実装します。

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize);

availableSizeは親要素が提供可能なサイズですので、これをもとに必要サイズを計算し、その値を返します。

2. Arrange

親要素はDesiredSizeをもとに各子要素の配置を決定し、子要素に通知する手続きです。

親要素は子要素の配置が決まると、子要素のArrangeメソッドを呼びます。そうすると子要素はRenderSizeプロパティを更新します。FrameworkElementを継承したクラスを実装するうえでは、ArrangeOverrideメソッドをオーバーライドすることでその手続きを実装します。

protected virtual System.Windows.Size ArrangeOverride(System.Windows.Size finalSize);

finalSizeは実際に自分に割り当てられた大きさですので、これをもとに自身の配置を制御します。返却値は実際のサイズになりますが、まあ、普通は割り当てられたサイズそのままになると思いますので、そのような場合はこのメソッドを敢えてオーバーライドする必要はありません。FrameworkElementがこの返却値をもとにRenderSizeプロパティを更新してくれます。

子要素の申告でレイアウト変更を行う

ここまで説明してきたのは、すべて親要素起点のレイアウト変更です。例えばウィンドウサイズの変更による配置変更などですね。ただ、コントロールを実装するうえでは、自身のサイズが変わるなどして親要素にレイアウト変更を依頼しなければならないことが出てきます。例えば表示する文字列が変わった、文字サイズが変わったなどですね。そのような場合はどうすれば良いのでしょうか。

一つの方法としては、InvalidateMeasureメソッドを呼ぶことです。これにより現在のMeasureの結果を無効なものとし、再度レイアウトプロセスを実行することを促せます。
ただしMSDNの説明文にも書いてある通り、このメソッドの頻繁な呼び出しはパフォーマンスに大きな影響を与えるため、可能な限り呼び出しは避けるべきと書かれております。もっと良い方法がほかにあるのでしょうか。

前述したとおり、レイアウト変更が必要になるケースとして、文字列の変更や文字サイズの変更などが考えられます。それらは、一般にコントロールの依存関係プロパティ(Dependency Property)として外に公開されていることが多いです。そこに、そのプロパティがMeasureに影響する項目かどうかを設定する機能があるのです。

public bool AffectsMeasure
{
    get { return (bool)GetValue(AffectsMeasureProperty); }
    set { SetValue(AffectsMeasureProperty, value); }
}
public static readonly DependencyProperty AffectsMeasureProperty =
    DependencyProperty.Register(nameof(AffectsMeasure), typeof(bool), typeof(FrameworkElementDerivedClass),
    new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure));

依存関係プロパティを実装するときはだいたいこんな形になると思います。
このFrameworkPropertyMetadataに渡している引数に注目です。FrameworkPropertyMetadataOptions.AffectsMeasureというのを渡しています。こうすることで、このプロパティが変化したときに自動的にMeasureプロセスが実施されるようになります。
他にもAffectsArrange、AffectsParentMeasure、AffectsParentArrangeがあり、名前の通りです。

 

描画処理

レイアウトに対応できるようになったところで、次は描画処理です。
これもいろいろな方法があるようですが、一番シンプルでかつ上のレイアウト変更と親和性の高い方法を紹介します。

OnRenderメソッド

FrameworkElementにはOnRenderメソッドがあり、これをオーバーライドするのが最もシンプルです。

protected virtual void OnRender(System.Windows.Media.DrawingContext drawingContext);

このメソッドでDrawingContextが渡されます。これがいわゆるWin32で言うところのデバイスコンテキストハンドルみたいなもので、これに対して描画操作をすることで画面に描画することができます。

DrawingContextが持っているメソッドのうちDrawから始まるものを見ればわかりますが、直線、四角形、楕円、ジオメトリ、画像、文字列、グリフなど一通りの描画メソッドを持っています。
また、Pushから始まるものを見ると、クリッピング、Opacity(透明性)、Transform(図形変換)などもサポートしています。Popメソッドもあある通りスタック構造をしており、PushしてからPopされるまでの間それらの処理が適用されるようです。

再描画指示

さて、上述のレイアウトプロセス(Measure→Arrange)の後に自動的にOnRenderが呼ばれるのは言うまでも無いですが、それ以外のときも再描画したいシチュエーションはあります。
これもレイアウトプロセスと同じで、InvalidateVisualメソッドを呼び出して再描画させることができますが、同様にパフォーマンスに影響を与えるので基本は呼ぶべきではないものです。
描画に影響を与えるプロパティにFrameworkPropertyMetadataOptions.AffectsRenderオプションを渡してやれば良いでしょう。


プロパティ値の継承

WPFは依存関係プロパティの値の継承機能があります。一番身近なのはDataContextで、親要素で設定したDataContextが子要素のDataContextにアクセスすることで触れるのはよく知られていると思います。これはDataContextだけでなく、例えばフォントサイズなども親要素で設定したら子要素にも伝搬します。これを「プロパティ値の継承(Property value inheritance)」と言います。

そのような依存関係プロパティを実装する際は、FrameworkPropertyMetadataのコンストラクタにFrameworkPropertyMetadataOptions.Inheritsを渡せば良いです。こうすることで、親要素で設定されたものが子要素に伝搬してきます。

ちなみに、例えばフォントファミリーやフォントサイズなどの文字列関係のプロパティは、TextElementクラスなんかが使えるようです。このクラスに文字列関係の依存関係プロパティのフィールドがありますので、それを使えば簡単にフォント関係のプロパティ値の継承ができます。

public FontFamily? FontFamily
{
    get { return (FontFamily?)GetValue(FontFamilyProperty); }
    set { SetValue(FontFamilyProperty, value); }
}
public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TategakiText));

この依存関係プロパティはちゃんと上述のAffects***などのオプションもちゃんと実装されているようで、これだけでレイアウトやレンダリングも問題なしです。

 

まとめ

さて、多分滅多に使うことのない、WPFのレイアウトシステムと描画処理に関する実装の仕方をまとめました。滅多に使うことが無いからかちゃんとまとまって解説してくれている資料があまり無いんですよね。この点からも.NETのソースコードが公開されるようになったのは、それを見ることである程度どんな感じか追えるのでとても助かります。

最後に、こうやって作られたのがWPF縦書きライブラリ Tategakiです。こっちもよろしくね!

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

昨日3.0.0を公開したばかりですが、早速ver.3.0.1を公開です。
ターゲットに.NET Framework 4.7.2を追加しました。処理は変えていませんが、.NET Frameworkだと最新のC#の機能が一部使えなかったりしますので、ソースコードは一部いじっています。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

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

先日、ふとTwitter経由でTategakiの不具合についての報告がありました。前回更新からすでに8年以上経過しているのに、こうやって連絡をいただけるのはとても嬉しいことです。 

(ライブラリをダウンロードしたいだけの人、使い方を見たいだけの人は下のほうのGithubリンクからリポジトリにアクセスしてください)

不具合の経緯

不具合の内容ですが、 TategakiText ver.2系はコントロールをカスタムコントロールで作っており、コントロール内部にGlyphsコントロールを配置することで縦書きを実現していました。これがDocumentViewerコントロールと相性が悪いようで、DocumentViewer上でTategakiText上にマウスオーバーすると例外を吐いて落ちるようでした。

報告してくださった方は、対策案として、ControlではなくContentControlでGlyphsをホスティングするということまで提案してくださっていました。
ここまでいろいろとやってくれる方がいるのに知らんぷりするわけにはいかない…!というわけで、本腰を入れて不具合の解消を始めました。

不具合内容

FixedPageに配置したコントロールでマウスオーバーが発生すると、そのコントロールから親要素を辿ってFixedPageまでの関係を確認するようですが、そこでGlyphs→Borderときて、その次Contorlに行けず(LogicalTreHelper.GetParent(border)がnullとなり)ArgumentNullExceptionが発生しているようでした。

.NETはオープンソースでコードが公開されているので、当該部位を見てみましょう。

// We have no uniform way to do random access for this element.
// This should never happen for S0 conforming document.

(◞‸◟)(◞‸◟)(◞‸◟)(◞‸◟)(◞‸◟)

"S0 confirming document"(S0準拠ドキュメント?)が何を指すかよくわかりませんが、「この分岐に来ることは無いから実装は適当で良いよね、エイヤッ!」で実装されて、nullだった場合の対処とかは深く考えられていなかったコードということですね。

対応方針とやりたかったこと

さて、報告してくださった方は前述の通りContentControlにしたらこの不具合は発生しなくなったと連絡してくれているのですが、Tategakiライブラリが目指すのは「TextBlockの縦書き版」です。
ContentControlはTemplateが使えるという特徴があり、WPFではTextBlockというよりかLabelがそれに該当します。ですので、せっかく提案いただいていて申し訳ないのですが、少しコンセプトからずれてしまうなと思い、別の方法で対処できないか検討することとしました。

そもそも、元祖TextBlockはカスタムコントロールを使っているわけでもGlyphsをラッピングしているわけでもなく、直接FrameworkElementを継承したクラスとして実装されています。従って、現在のTategakiTextの実装方法自体がそもそもコンセプトずれしているということで、直接FrameworkElementを継承することで実装するという方法で進めていくこととしました。
事の発端となった不具合も、呼び出し経路の中でマウスオーバーされたのがGlyphsコントロールかどうかの分岐が入っています。実装を変えれば不具合も解消するでしょう。

実装

FrameworkElementを直接継承して描画処理を行うのはドキュメントが少なく四苦八苦したのですが、それ自体が別記事となるような分量ですので、後ほど別記事として紹介します。

公開

今まではこのブログにファイルを添付する形でソフトを公開していましたが、意外と私の見えないところで使ってくださる方もいて、でも不具合を踏んだけど報告もできずに困っている方も実はいたのではないかと今回の連絡から反省しました。

私がプログラミングを始めたのは、いわゆるVector / 窓の杜時代で、個人が作ったソフトがクローズドソースでそういったホスティングサービスを経由して公開されている時代でした。でも今は違います。オープンソースでコミュニティがいろいろとフィードバックをくれる時代です。
ならばやはりそういうやり方で公開するほうが良いのではないか、ということで、Githubのリポジトリを立ち上げました。

 

これならば不具合があればIssueを立てれるし、バグを直してくれる方がいればPullRequestをくれるはず…!
実を言うとGithubは使うのがこれがほぼ初めてです。初心者ですがよろしくお願いします!!!!!

もちろん従来通りNugetでも公開しております。

Tategaki - Nuget

前回はターゲットが.NET Framework 4.0でしたが、今回から.NET 6にしています。.NET Framework系も必要な方がいればコメント欄か、コードを修正してPullRequestで連絡ください。検討します。

2023年5月13日土曜日

ネイピア数を計算する その2(ネイピア数6560万桁もあるよ!)

前回の記事でネイピア数の計算について少し書きました。計算の仕方の見通しが付くと、次は速さを求めてみたくなってきますよね。ということで、今回は本気でソフトを作りこんでいきます。

式変形

まずは式をできるだけコンピューターで計算しやすい形に変形して無駄な計算を減らします。おさらいですが、ネイピア数は次の範囲にあります。

\[1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}+\frac{1}{(n+1)!}<e<1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}+\frac{e}{(n+1)!}\]

まずは左半分を整理します。

\[\begin{eqnarray*}1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}+\frac{1}{(n+1)!}&<&e\\\frac{{}_{n+1}P_{n+1}+{}_{n+1}P_{n}+_{n+1}P_{n-1}+\cdots+{}_{n+1}P_{1}+1}{(n+1)!}&<&e\end{eqnarray*}\]

ただし${}_nP_r$は順列です。高校の数学でよく出てくるやつです。

つづいて右半分を整理します。

\[\begin{eqnarray*}e&<&1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}+\frac{e}{(n+1)!}\\\left\{1-\frac{1}{(n+1)!}\right\}e&<&1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}\\e&<&\left(1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}\right)\frac{(n+1)!}{(n+1)!-1}\\e&<&\frac{{}_{n}P_{n}+{}_{n}P_{n-1}+_{n}P_{n-2}+\cdots+{}_{n}P_{1}+1}{n!}\frac{(n+1)!}{(n+1)!-1}\\e&<&\frac{\left({}_{n}P_{n}+{}_{n}P_{n-1}+_{n}P_{n-2}+\cdots+{}_{n}P_{1}+1\right)(n+1)}{(n+1)!-1}\\e&<&\frac{{}_{n+1}P_{n+1}+{}_{n+1}P_{n}+_{n+1}P_{n-1}+\cdots+{}_{n+1}P_{1}}{(n+1)!-1}\end{eqnarray*}\]

勘の良いひとは気付くかもしれませんが、$S$と$F$を

\[\begin{eqnarray*}S&=&{}_{n+1}P_{n+1}+{}_{n+1}P_{n}+_{n+1}P_{n-1}+\cdots+{}_{n+1}P_{1}\\F&=&(n+1)!\end{eqnarray*}\]

と置くと、最初の不等式は次のように表せます。

\[\frac{S+1}{F}<e<\frac{S}{F-1}\]

ああ、なんと美しいのでしょう。

ちなみに、${}_{n}P_{k}=(n-k+1)\cdot{}_{n}P_{k-1}$ですので、$S$は右側の項から順に計算していけば、一項につき掛け算を1回するだけで求まっていきます。そして$n!={}_nP_n$ですので、Sの各項を求めると最後はFが求まることになります。事前に式変形で計算量を減らせるのはこのあたりが限界でしょう。

実装

さて、上の式変形をもとにプログラムを書いてみましょう。

static (Fraction lower, Fraction higher) NapierRange(int order)
{
    var permutation = BigInteger.One;
    var totalNumerator = BigInteger.Zero;


    for(int i = order + 1; i > 0; i--) {
        permutation *= i;
        totalNumerator += permutation;
    }

    var lower = new Fraction(totalNumerator + BigInteger.One, permutation);
    var higher = new Fraction(totalNumerator, permutation - BigInteger.One);

    return (lower, higher);
}

こんな感じでしょうか。要するにtotalNumeratorは$S$、permutationは$F$を意味しています。

なお、前回の記事では以下のようなプログラムを書きました。

static (Fraction lower, Fraction higher) NapierRange_F(int order)
{
    Fraction napier = Fraction.One;
    Fraction factorial = Fraction.One;
    Fraction factorialFrom = Fraction.Zero;

    for(int i = 0; i < order; i++) {
        factorialFrom += Fraction.One;
        factorial *= factorialFrom;
        napier += Fraction.One / factorial;
    }

    factorialFrom += Fraction.One;
    factorial *= factorialFrom;

    Fraction lower = napier + Fraction.One / factorial;
    Fraction higher = napier * factorial / (factorial - Fraction.One);

    return (lower, higher);
}

どちらのメソッドでも同じ値を返してきますが、例えば、第1万項までを計算してみたところ、NapierRangeは0.12秒だったのに対し、NapierRange_Fは48秒でした。実に400倍も実行速度が違います。それだけ、通分や約分にはとてつもない計算量が必要ということですね。

並列化

さて、前回の記事より格段と速くなりましたが、もう一声いきたいですね。

現在の計算式はシングルスレッドです。ですが、近年のパソコンはCPUコアがたくさんついているので、並列化をできるならしたほうが計算速度の向上が見込めます。しかし、見ての通り、NapierRangeのforループは前回までの計算結果を参照しますので、各回が独立していません。すなわち、このままでは並列化できません。

ここで、順列の以下の性質を思い出してください。

\[{}_nP_k\cdot{}_{n-k}P_l={}_nP_{k+l}\]

これをうまく使えば、

\[{}_{n}P_{1}+{}_{n}P_{2}+\cdots+{}_{n}P_{n}=\left({}_{n}P_{1}+{}_{n}P_{2}+\cdots+{}_{n}P_{\lfloor n/2 \rfloor}\right)\left(1+{}_{\lceil n/2 \rceil}P_{1}+{}_{\lceil n/2 \rceil}P_{2}+\cdots+{}_{\lceil n/2 \rceil}P_{\lceil n/2 \rceil}\right)\]

と式変形できます。ただし$\lfloor \rfloor$は床関数、$\lceil \rceil$は天井関数です。こうすることで、左右のカッコ内の計算はそれぞれ独立で、計算量も均等です(任意のビット数を全く同じ速度で計算できるならばですが…実際はそんなことないのですが…)。今回は2分割ですが、うまくやれば好きな数で分割することができます。そうやって実装したのがこちらになります。

static (Fraction lower, Fraction higher) NapierRange(int order, int thread)
{
    var opt = new ParallelOptions() { MaxDegreeOfParallelism = thread };

    // 事前に通分すれば、分子は順列の総和になる
    // 分子をthreadの個数で分割し、並列で計算する
    var parallel = new (BigInteger permutation, BigInteger total)[thread];
    Parallel.For(0, thread, opt, t => {
        int from = (int)((long)order * (thread - t) / thread);
        int to = (int)((long)order * (thread - t - 1) / thread);

        BigInteger permutation = BigInteger.One;
        BigInteger totalNumerator = BigInteger.Zero;

        for(int i = from; i > to; i--) {
            permutation *= i;
            totalNumerator += permutation;
        }

        parallel[t].permutation = permutation;
        parallel[t].total = totalNumerator;
    });

    // 分割した分を統合する
    BigInteger permutation = BigInteger.One;
    BigInteger totalNumerator = BigInteger.One;
    for(int i = 0; i < thread; i++) {
        totalNumerator += parallel[i].total * permutation;
        permutation *= parallel[i].permutation;
    }

    // ここの時点でtotalNumeratorにはorder次までの分子の総和、permutationにはorder!が入っている

    // 分母分子を(order+1)倍するとこの後の計算が超綺麗になる
    permutation *= (order + 1);
    totalNumerator *= (order + 1);

    // 上限と下限は同時に計算する(独立なので)
    Fraction lower = default, higher = default;
    Parallel.Invoke(opt,
        () => lower = new Fraction(totalNumerator + BigInteger.One, permutation),
        () => higher = new Fraction(totalNumerator, permutation - BigInteger.One));

    return (lower, higher);
}

区間を分割して並列で計算しますが、そのあと、それぞれを掛け合わせる再は結局はループ計算になってしまいます。でも多少は速くなるようです。ついでに、時間がべらぼうにかかる約分も上限側と下限側で別々にするようにしました。

試しに第10万項まで計算してみると、シングルスレッド版で5.5秒、マルチスレッド版で12秒でした。倍くらい速くなりましたね。項数が増えてくると差が顕著になると思われます。

分数の十進小数化

さて、ネイピア数の範囲が分数で表せるようになったところで、今度はそれを小数にしてやらねばなりません。これはむしろネイピア数を求めるよりも重たい処理になります。

前回は以下のようなコードを書きました。

static IEnumerable<char> ToDecimalString(Fraction lower, Fraction higher)
{
    var lower_dec = lower.ToBigInteger();
    var higher_dec = higher.ToBigInteger();
    if(lower_dec == higher_dec) {
        foreach(char c in lower_dec.ToString())
            yield return c;
        yield return '.';
 
        var lower_rem = lower.Numerator;
        var higher_rem = higher.Numerator;
 
        while(true) {
            lower_rem = lower_rem % lower.Denominator * 10;
            higher_rem = higher_rem % higher.Denominator * 10;
 
            char lower_digit = (lower_rem / lower.Denominator).ToString()[0];
            char higher_digit = (higher_rem / higher.Denominator).ToString()[0];
 
            if(lower_digit != higher_digit)
                break;
            else
                yield return lower_digit;
        }
    }
}

大まかな流れはこれでOKです。いろいろ考えて試してみましたが、このパターンが一番速いようです。ですが、少しだけ追加で方針を加えます。

  • DivRemメソッドを使用する
  • 並列化をする
  • 1桁ずつじゃなくて、n桁(nは1000くらい)ずつ計算する

まずDivRemメソッドですが、除算をしたときに商と剰余を一緒に出してくるメソッドになります。C#には除算と剰余算で別々の演算子を持っていますが、これら2つは片方の計算をしようとするともう片方も必然的に出てくるものです。別々に計算していてはほぼ同じ計算を2度することになってしまいます。

並列化は字のままです。独立している計算、すなわち下限と上限それぞれを並列で計算しましょう。

最後ですが、10で割った商を次の桁、その余りを10倍してループの次へとやっていくと、1桁を求めるのに1回ずつ除算と掛け算をしなければならなくなってしまいます。しかし、それらはどちらも計算コストの高い計算です。そこで、100で割った商を次の2桁とすれば、除算や乗算をする回数を半分にできます。1000で割れば…と、ここの数を増やすことで格段と計算の回数を減らせるのです。その代わり商は大きくなってしまって、商を十進数にするのに少し時間がかかるようになってしまいますが、DivRemのほうが遥かに重い処理であるようです。そのため、1000桁、すなわち10^1000くらいで割って、一気に1000桁分を求めるほうが遥かに計算速度は速くなります。

そうして実装したコードが次となります。これは1000桁ごとです。digit_unitをいじることで何桁ごとか変えることはできます。

static IEnumerable<char> ToDecimalString(Fraction lower, Fraction higher, int thread)
{
    var opt = new ParallelOptions() { MaxDegreeOfParallelism = thread };

    const int digit_unit = 1000;
    BigInteger digitPow = BigInteger.Pow(10, digit_unit);

    // まずは整数部分を比較
    var lower_dec = lower.ToBigInteger();
    var higher_dec = higher.ToBigInteger();
    if(lower_dec == higher_dec) {
        foreach(char c in lower_dec.ToString())
            yield return c;
        yield return '.';

        var lower_deno = lower.Denominator;
        var higher_deno = higher.Denominator;

        var lower_rem = lower.Numerator % lower_deno;
        var higher_rem = higher.Numerator % higher_deno;

        while(true) {
            string lower_digit_str = "", higher_digit_str = "";

            // DivRemの処理がほかの処理の500倍くらい時間かかるので、これ以上の高速化は難しい。
            Parallel.Invoke(opt, () => {
                (var lower_digit, lower_rem) = BigInteger.DivRem(lower_rem * digitPow, lower_deno);
                lower_digit_str = lower_digit.ToString().PadLeft(digit_unit, '0');
            }, () => {
                (var higher_digit, higher_rem) = BigInteger.DivRem(higher_rem * digitPow, higher_deno);
                higher_digit_str = higher_digit.ToString().PadLeft(digit_unit, '0');
            });

            foreach(var (lower_c, higher_c) in lower_digit_str.Zip(higher_digit_str)) {
                if(lower_c == higher_c)
                    yield return lower_c;
                else
                    yield break;                        
            }
        }
    }
}

意外とこれで速くなって、従来コードだと第10万項まで(=45.6万桁)の計算で380秒かかりましたが、今回のコードで11秒まで縮まりました。38倍とは…かなりですね。

1000万項(約6560万桁)への挑戦

ここまで計算が速くなったら、大きな桁数のネイピア数に挑戦してみたくなりますよね。ただし計算量は桁数が増えるとともに爆発的に増えていきます。

ということで、1000万項までの計算をしてみました。それで求まったネイピア数は65,657,065桁、計算には丸々4日かかりました。

こちらからダウンロードしてください。

 65.6 million digits.zip (33.1MB)

1億桁くらい計算してみたい気もしますが、結構時間がかかりそうですのでまあ何かの機会にでも。

2023年5月4日木曜日

ネイピア数を数値計算する

ふと1年近くブログ書いていないなーと思い、何かちょうどいいネタをと探していたところ、C#11の新機能であるGeneric Mathを知りました。ということで、ネイピア数の数値計算をネタにGeneric Mathを触っていきます。

ネイピア数

個人的には「自然対数の底」という表現のほうが慣れているのですが、あまり名前っぽい感じがしないので「ネイピア数」と呼びます。

ネイピア数というのは、以下の定義で与えられる定数(超越数)です。

\[ \lim_{n \to \infty} \left(1+\frac{1}{n}\right)^n\]

値はだいたい2.718281828です。

この数には特別な性質があり、この数を底に取った指数関数は、何度微分しても同じ(形が変わらない)のです。

\[\left(e^x\right)'=e^x\]

この数を数値計算で求めていきましょう。

テイラーの定理

テイラーの定理とは、テイラー展開を有限の項で打ち止めにしたものです。剰余項というものがつき、それによって近似の誤差が評価できるので、このような数値計算ネタに持ってこいです。

\[\begin{eqnarray*}f(x)&=&\sum_{k=0}^{n}\frac{f^{(k)}(a)}{k!}(x-a)^k+R_n(x)\\&=&f(a)+\frac{f'(a)}{1!}(x-a)+\frac{f''(a)}{2!}(x-a)^2+\cdots+\frac{f^{(n)}(a)}{n!}(x-a)^n+R_n(x)\end{eqnarray*}\]

この$R_n(x)$が剰余項で、以下の通り表されます。

\[R_n(x)=\frac{f^{(n+1)}(c)}{(n+1)!}(x-a)^{n+1}\]

ただし、$c$は$a\gtrless c\gtrless x$を満たす数です。$\gtrless$という見慣れない不等号を書いてしまいましたが、要するに$a$と$x$の間です。$a$と$x$どちらが大きいかわからないのでこういう書き方になってしまっています。

見てのとおり、テイラー展開を第n項までで打ち止めた場合、真の値との誤差は$R_n(x)$内にあるわけです。すなわち、$R_n(x)$の大きさがわかればテイラー展開の精度もわかり、テイラー展開にて小数第何位まで値が求められたかを評価することができるようになるのです。

指数関数$e^x$のテイラー展開

指数関数のテイラー展開はまず真っ先に習うものですね。なんて言ったって、何階微分しても式の形が変わらないのですから。

\[e^x=1+\frac{1}{1!}x+\frac{1}{2!}x^2+\cdots+\frac{1}{n!}x^n+\frac{e^c}{(n+1)!}x^{n+1}\]

これは上式を$a=0$周りのテイラー展開をしたものになります。ネイピア数を求めたい場合は、これに$x=1$を代入すればいいことがわかります。

\[e=1+\frac{1}{1!}+\frac{1}{2!}+\cdots+\frac{1}{n!}+\frac{e^c}{(n+1)!}\]

$c$は$a<c<x$を満たす、すなわち$0<c<1$を満たしますので、$e^x$が単調増加であることを踏まえると、剰余項の最小値は$1/(n+1)!$、最大値は$e/(n+1)!$となります。これによって、テイラー展開を第n項で打ち止めした際の値の範囲がわかるようになりました。

手計算で求めてみる

さて、実際に数値計算をするとどうなるか、多少手計算をやってみます。

第1項までの場合

\[\begin{eqnarray*}e&=&1+\frac{1}{1!}+\frac{e^c}{2!}\\&=&2+\frac{e^c}{2}\end{eqnarray*}\]

よって

\[2+\frac{1}{2}<e<2+\frac{e}{2}\]

右半分を整理すると、最終的にこの不等式は以下の通りになります。

\[2.5<e<4\]

第2項までの場合

\[\begin{eqnarray*}e&=&1+\frac{1}{1!}+\frac{1}{2!}+\frac{e^c}{3!}\\&=&\frac{5}{2}+\frac{e^c}{6}\end{eqnarray*}\]

よって

\[\frac{5}{2}+\frac{1}{6}<e<\frac{5}{2}+\frac{e}{6}\]

同様に整理して

\[2.667<e<3\]

第3項までの場合

\[\begin{eqnarray*}e&=&1+\frac{1}{1!}+\frac{1}{2!}+\frac{1}{3!}+\frac{e^c}{4!}\\&=&\frac{8}{3}+\frac{e^c}{24}\end{eqnarray*}\]

よって

\[\frac{8}{3}+\frac{1}{24}<e<\frac{8}{3}+\frac{e}{24}\]

計算がしんどくなってきましたが、同様に整理して

\[2.7083<e<2.7826\]

このようにどんどん範囲が狭まってきています。第3項の結果から、ネイピア数の小数第1位までの表現(小数第2位以下切り捨て)が2.7であることは間違いありません。このようにして、ネイピア数を数値で表現するといくつかというのを計算できるのです。

C#での実装

さて、前置きが長くなりましたが、これをC#で実装してみます。前述のとおり、C#11で導入されたGeneric Mathの機能を使って記述してみます。

static (T lower, T higher) NapierRange<T>(int order) where T : INumber<T>
{
    T napier = T.One;
    T factorial = T.One;
    T factorialFrom = T.Zero;

    for(int i = 0; i < order; i++) {
        factorialFrom++;
        factorial *= factorialFrom;
        napier += T.One / factorial;
    }

    factorialFrom++;
    factorial *= factorialFrom;

    T lower = napier + T.One / factorial;
    T higher = napier * factorial / (factorial - T.One);

    return (lower, higher);
}

こんな感じでどうでしょう。第何項までテイラー展開を計算するかを与えたら、ネイピア数が持ちうる範囲を返すメソッドです。INumber<T>インターフェースはC#11(.NET 7)より導入されたインターフェースで、数値計算をサポートする型(doubleやintなど)が実装するインターフェースとなります。これを使うことで抽象的な計算を実装することができますが、数値リテラルが使えなくなるため、T.OneやT.Zeroなどを使って上手いこと実装する必要があります。ちょいと一癖ありますね。

呼び出し側はこんな感じになります。

for(int i = 1; i < 27; i++) {
    var (lower, higher) = NapierRange<double>(i);

    var result = new string(lower.ToString().Zip(higher.ToString()).TakeWhile(p => p.First == p.Second).Select(p => p.First).ToArray());
    if(result.Length == 0)
        Console.WriteLine($"{i}: {lower} - {higher}");
    else
        Console.WriteLine($"{i}: {result} (Error: {higher - lower})");
}

確定した桁が無い場合は範囲で、確定した桁があればその桁と誤差の幅を表示します。NapierRangeメソッドに渡す型引数でどの型で計算するか簡単に切り替えられるのが、このGeneric Mathの最大の魅力です。

Halfでの計算結果

つい最近知ったのですが、いつの間にか(.NET5かららしいです)Half型(半精度浮動小数点型)が.NETに実装されていたのですね。あえてfloatより精度の悪い計算を使う必要がある場面があるのか私にはよくわかりませんが…。組み込み環境などの貧弱なCPUを想定しているのでしょうか。

1: 2.5 - 4
2: 2.666 - 3
3: 2.7 (Error: 0.0762)
4: 2.7 (Error: 0.01367)
5: 2.71 (Error: 0.001953)
6: 2.717 (Error: 0)
7: 2.717 - ∞
8: 2.717 - NaN
9: 2.717 - NaN
10: 2.717 - NaN

見てのとおり、第6項まででサチっちゃっています。小数第3位まで計算できたかと思いきや、正しいネイピア数は2.71828…ですので小数第3位の値が間違っています。そもそもの小数の計算の誤差(コンピューターによる浮動小数点演算での誤差)が大きく、正しく計算できているのは小数第2位までということですね。

floatでの計算結果

続いて単精度浮動小数点型ことfloatでの計算結果です。

1: 2.5 - 4
2: 2.6666667 - 3
3: 2.7 (Error: 0.074275255)
4: 2.7 (Error: 0.014425755)
5: 2.7 (Error: 0.0023896694)
6: 2.718 (Error: 0.000341177)
7: 2.718 (Error: 4.2676926E-05)
8: 2.71828 (Error: 4.7683716E-06)
9: 2.718282 (Error: 4.7683716E-07)
10: 2.718282 (Error: 0)

今度は第10項で誤差がゼロになりました。ただし第9項と得られる小数位は変わらず、第6位までです。

doubleでの計算結果

続いて倍精度浮動小数点型ことdoubleでの計算結果です。

1: 2.5 - 4
2: 2.6666666666666665 - 3
3: 2.7 (Error: 0.0742753623188408)
4: 2.7 (Error: 0.014425770308123198)
5: 2.7 (Error: 0.0023895070313706412)
6: 2.718 (Error: 0.0003409910633562774)
7: 2.718 (Error: 4.26170978902185E-05)
8: 2.71828 (Error: 4.735136300837439E-06)
9: 2.71828 (Error: 4.7351253140703875E-07)
10: 2.7182818 (Error: 4.3046583630967916E-08)
11: 2.7182818 (Error: 3.5872149695137523E-09)
12: 2.718281828 (Error: 2.759392714324349E-10)
13: 2.7182818284 (Error: 1.971001140077533E-11)
14: 2.7182818284 (Error: 1.3140599719463353E-12)
15: 2.718281828459 (Error: 8.171241461241152E-14)
16: 2.7182818284590 (Error: 4.884981308350689E-15)
17: 2.71828182845904 (Error: 4.440892098500626E-16)
18: 2.7182818284590455 (Error: 0)

今度は第18項で誤差がゼロになりました。小数16位まで計算できたかと思いきや、こちらもHalf型と同じく誤差が発生しており、正しいネイピア数は2.71828182845904523536…です。正しく計算できているのは小数第15位までですね。

decimalでの計算結果

続いて十進浮動小数点型ことdecimalでの計算結果です。

1: 2.5 - 4
2: 2.6666666666666666666666666667 - 3.0
3: 2.7 (Error: 0.0742753623188405797101449275)
4: 2.7 (Error: 0.0144257703081232492997198880)
5: 2.7 (Error: 0.0023895070313707309534847782)
6: 2.718 (Error: 0.0003409910633566120765962004)
7: 2.718 (Error: 0.0000426170978903561556901173)
8: 2.71828 (Error: 0.0000047351363004560535050496)
9: 2.71828 (Error: 0.0000004735125315969235220021)
10: 2.7182818 (Error: 0.0000000430465836250670517231)
11: 2.7182818 (Error: 0.0000000035872152240689462999)
12: 2.718281828 (Error: 0.0000000002759396321147182670)
13: 2.7182818284 (Error: 0.0000000000197099737196723090)
14: 2.7182818284 (Error: 0.0000000000013139982479646711)
15: 2.718281828459 (Error: 0.0000000000000821248904977353)
16: 2.71828182845904 (Error: 0.0000000000000048308759116312)
17: 2.718281828459045 (Error: 0.0000000000000002683819950905)
18: 2.7182818284590452 (Error: 0.0000000000000000141253681627)
19: 2.71828182845904523 (Error: 0.0000000000000000007062684082)
20: 2.7182818284590452353 (Error: 0.0000000000000000000336318289)
21: 2.71828182845904523536 (Error: 0.0000000000000000000015287196)
22: 2.718281828459045235360 (Error: 0.0000000000000000000000664661)
23: 2.7182818284590452353602 (Error: 0.0000000000000000000000027694)
24: 2.718281828459045235360287 (Error: 0.0000000000000000000000001108)
25: 2.71828182845904523536028747 (Error: 0.0000000000000000000000000042)
26: 2.718281828459045235360287471 (Error: 0.0000000000000000000000000001)

第26項までの計算でも誤差がゼロになっていませんが、第27項を計算しようとすると、階乗計算で桁あふれが発生して例外が発生してしまいました。小数第27位まで正しく計算できました。

番外編:無限精度分数型での実装

さて、今までの計算を見てきてもわかったと思いますが、浮動小数点型は計算誤差が発生するため、いくら剰余項で誤差を見積もったとしても計算上の誤差で数値が信用無くなってしまいます。一方で、指数関数のテイラー展開は分数の多項式で表されるため、分数を分数のまま計算できるような枠組みがあれば計算上の精度の問題は発生しなくなるはずです。

ということで少し調べてみたところ、Fractionsというライブラリが有名どころみたいです。.NET備え付けのBigInteger型(無限精度整数型)を用いて、計算上の劣化の一切ない分数演算をすることができます。ただし、Fractionsライブラリは.NET7のGeneric Mathには対応していないようで、上で示した実装の型引数をFraction型に書き換えたメソッドを新たに作ってやる必要があります。

static (Fraction lower, Fraction higher) NapierRange_F(int order)
{
    Fraction napier = Fraction.One;
    Fraction factorial = Fraction.One;
    Fraction factorialFrom = Fraction.Zero;

    for(int i = 0; i < order; i++) {
        factorialFrom += Fraction.One;
        factorial *= factorialFrom;
        napier += Fraction.One / factorial;
    }

    factorialFrom += Fraction.One;
    factorial *= factorialFrom;

    Fraction lower = napier + Fraction.One / factorial;
    Fraction higher = napier * factorial / (factorial - Fraction.One);

    return (lower, higher);
}

なお、Fractionsライブラリにはその分数の値を任意の精度(桁数)の小数で文字列化する機能が無いようなので、それはこちらで実装してやる必要があります。今回の目的では、ネイピア数の下限と上限が出てきますので、それぞれの桁の値が同じである限り出力を続けるメソッドを作りました。

static IEnumerable<char> ToDecimalString(Fraction lower, Fraction higher)
{
    var lower_dec = lower.ToBigInteger();
    var higher_dec = higher.ToBigInteger();
    if(lower_dec == higher_dec) {
        foreach(char c in lower_dec.ToString())
            yield return c;
        yield return '.';

        var lower_rem = lower.Numerator;
        var higher_rem = higher.Numerator;

        while(true) {
            lower_rem = lower_rem % lower.Denominator * 10;
            higher_rem = higher_rem % higher.Denominator * 10;

            char lower_digit = (lower_rem / lower.Denominator).ToString()[0];
            char higher_digit = (higher_rem / higher.Denominator).ToString()[0];

            if(lower_digit != higher_digit)
                break;
            else
                yield return lower_digit;
        }
    }
}

yield returnを使って1桁(1文字)ずつ値を返していくようなコードにしています。

呼び出し側のコードはこんな感じにしてみました。

const int order = 10000;
bool dot = false;
int digit = 0;
var buffer = new StringBuilder("e = ");
var sw = new Stopwatch();

Console.WriteLine($"Order: {order}");
sw.Start();

var (lower, higher) = NapierRange_F(order);

sw.Stop();
Console.WriteLine($"Calculation Time: {sw.Elapsed.TotalSeconds} seconds");
sw.Reset();
sw.Start();

foreach(var c in ToDecimalString(lower, higher)) {
    buffer.Append(c);

    if(dot) {
        digit++;
        if(digit % 1000 == 0)
            buffer.AppendLine();
        if(digit % 50 == 0)
            buffer.AppendLine();
        else if(digit % 10 == 0)
            buffer.Append(' ');
    }
    if(c == '.') {
        dot = true;
        buffer.AppendLine();
    }
}
sw.Stop();
Console.WriteLine($"Digit: {digit}");
Console.WriteLine($"Decimalize Time: {sw.Elapsed.TotalSeconds} seconds");
Console.WriteLine();
Console.WriteLine(buffer.ToString());

1桁ずつ値が返ってくるので、10桁ごとにスペース、50桁ごとに改行、1000桁ごとに空行を入れるようにしています。

Order: 10000
Calculation Time: 80.7424568 seconds
Digit: 35662
Decimalize Time: 5.0592927 seconds

e = 2.
7182818284 5904523536 0287471352 6624977572 4709369995
9574966967 6277240766 3035354759 4571382178 5251664274
2746639193 2003059921 8174135966 2904357290 0334295260
5956307381 3232862794 3490763233 8298807531 9525101901
1573834187 9307021540 8914993488 4167509244 7614606680
8226480016 8477411853 7423454424 3710753907 7744992069
5517027618 3860626133 1384583000 7520449338 2656029760
6737113200 7093287091 2744374704 7230696977 2093101416
9283681902 5515108657 4637721112 5238978442 5056953696
7707854499 6996794686 4454905987 9316368892 3009879312
7736178215 4249992295 7635148220 8269895193 6680331825
2886939849 6465105820 9392398294 8879332036 2509443117
3012381970 6841614039 7019837679 3206832823 7646480429
5311802328 7825098194 5581530175 6717361332 0698112509
9618188159 3041690351 5988885193 4580727386 6738589422
8792284998 9208680582 5749279610 4841984443 6346324496
8487560233 6248270419 7862320900 2160990235 3043699418
4914631409 3431738143 6405462531 5209618369 0888707016
7683964243 7814059271 4563549061 3031072085 1038375051
0115747704 1718986106 8739696552 1267154688 9570350354

0212340784 9819334321 0681701210 0562788023 5193033224
7450158539 0473041995 7777093503 6604169973 2972508868
7696640355 5707162268 4471625607 9882651787 1341951246
6520103059 2123667719 4325278675 3985589448 9697096409
7545918569 5638023637 0162112047 7427228364 8961342251
6445078182 4423529486 3637214174 0238893441 2479635743
7026375529 4448337998 0161254922 7850925778 2562092622
6483262779 3338656648 1627725164 0191059004 9164499828
9315056604 7258027786 3186415519 5653244258 6982946959
3080191529 8721172556 3475463964 4791014590 4090586298
4967912874 0687050489 5858671747 9854667757 5732056812
8845920541 3340539220 0011378630 0945560688 1667400169
8420558040 3363795376 4520304024 3225661352 7836951177
8838638744 3966253224 9850654995 8862342818 9970773327
6171783928 0349465014 3455889707 1942586398 7727547109
6295374152 1115136835 0627526023 2648472870 3920764310
0595841166 1205452970 3023647254 9296669381 1513732275
3645098889 0313602057 2481765851 1806303644 2812314965
5070475102 5446501172 7211555194 8668508003 6853228183
1521960037 3562527944 9515828418 8294787610 8526398139

5599006737 6482922443 7528718462 4578036192 9819713991
4756448826 2603903381 4418232625 1509748279 8777996437
3089970388 8677822713 8360577297 8824125611 9071766394
6507063304 5279546618 5509666618 5664709711 3444740160
7046262156 8071748187 7844371436 9882185596 7095910259
6862002353 7185887485 6965220005 0311734392 0732113908
0329363447 9727355955 2773490717 8379342163 7012050054
5132638354 4000186323 9914907054 7977805669 7853358048
9669062951 1943247309 9587655236 8128590413 8324116072
2602998330 5353708761 3893963917 7957454016 1372236187
8936526053 8155841587 1869255386 0616477983 4025435128
4396129460 3529133259 4279490433 7299085731 5802909586
3138268329 1477116396 3370924003 1689458636 0606458459
2512699465 5724839186 5642097526 8508230754 4254599376
9170419777 8008536273 0941710163 4349076964 2372229435
2366125572 5088147792 2315197477 8060569672 5380171807
7636034624 5927877846 5850656050 7808442115 2969752189
0874019660 9066518035 1650179250 4619501366 5854366327
1254963990 8549144200 0145747608 1930221206 6024330096
4127048943 9039717719 5180699086 9986066365 8323227870

9376502260 1492910115 1717763594 4602023249 3002804018
6772391028 8097866605 6511832600 4368850881 7157238669
8422422010 2495055188 1694803221 0025154264 9463981287
3677658927 6881635983 1247788652 0141174110 9136011649
9507662907 7943646005 8519419985 6016264790 7615321038
7275571269 9251827568 7989302761 7611461625 4935649590
3798045838 1823233686 1201624373 6569846703 7858533052
7583333793 9907521660 6923805336 9887956513 7285593883
4998947074 1618155012 5397064648 1719467083 4819721448
8898790676 5037959036 6967249499 2545279033 7296361626
5897603949 8576741397 3594410237 4432970935 5477982629
6145914429 3645142861 7158587339 7467918975 7121195618
7385783644 7584484235 5558105002 5611492391 5188930994
6342841393 6080383091 6628188115 0371528496 7059741625
6282360921 6807515017 7725387402 5642534708 7908913729
1722828611 5159156837 2524163077 2254406337 8759310598
2676094420 3261924285 3170187817 7296023541 3060672136
0460003896 6109364709 5141417185 7770141806 0644363681
5464440053 3160877831 4317444081 1949422975 5993140118
8868331483 2802706553 8330046932 9011574414 7563139997

2217038046 1709289457 9096271662 2607407187 4997535921
2756084414 7378233032 7033016823 7193648002 1732857349
3594756433 4129943024 8502357322 1459784328 2641421684
8787216733 6701061509 4243456984 4018733128 1010794512
7223737886 1260581656 6805371439 6127888732 5273738903
9289050686 5324138062 7960259303 8772769778 3792868409
3253658807 3398845721 8746021005 3114833513 2385004782
7169376218 0049047955 9795929059 1655470505 7775143081
7511269898 5188408718 5640260353 0558373783 2422924185
6256442550 2267215598 0274012617 9719280471 3960068916
3828665277 0097527670 6977703643 9260224372 8418408832
5184877047 2638440379 5301669054 6593746161 9323840363
8931313643 2713768884 1026811219 8912752230 5625675625
4701725086 3497653672 8860596675 2740868627 4079128565
7699631378 9753034660 6166698042 1826772456 0530660773
8996242183 4085988207 1864682623 2150802882 8635974683
9654358856 6855037731 3129658797 5810501214 9162076567
6995065971 5344763470 3208532156 0367482860 8378656803
0730626576 3346977429 5634643716 7093971930 6087696349
5328846833 6130388294 3104080029 6873869117 0666661468

0001512114 3442256023 8744743252 5076938707 7775193299
9421372772 1125884360 8715834835 6269616619 8057252661
2206797540 6210620806 4988291845 4395301529 9820925030
0549825704 3390553570 1686531205 2649561485 7249257386
2069174036 9521353373 2531666345 4665885972 8665945113
6441370331 3936721185 6955395210 8458407244 3238355860
6310680696 4924851232 6326995146 0359603729 7253198368
4233639046 3213671011 6192821711 1502828016 0448805880
2382031981 4930963695 9673583274 2024988245 6849412738
6056649135 2526706046 2344505492 2758115170 9314921879
5927180019 4096886698 6837037302 2004753143 3818109270
8030017205 9355305207 0070607223 3999463990 5713115870
9963577735 9027196285 0611465148 3752620956 5346713290
0259943976 6311454590 2685898979 1158370934 1937044115
5121920117 1648805669 4593813118 3843765620 6278463104
9034629395 0029458341 1648241149 6975832601 1800731699
4373935069 6629571241 0273239138 7417549230 7186245454
3222039552 7352952402 4590380574 4502892246 8862853365
4221381572 2131163288 1120521464 8980518009 2024719391
7105553901 1394331668 1515828843 6876069611 0250517100

7392762385 5533862725 5353883096 0671644662 3709226468
0967125406 1869502143 1762116681 4009759528 1493907222
6011126811 5310838731 7617323235 2636058381 7315103459
5736538223 5349929358 2283685100 7810884634 3499835184
0445170427 0189381994 2434100905 7537625776 7571118090
0881641833 1920196262 3416288166 5213747173 2547772778
3488774366 5188287521 5668571950 6371936565 3903894493
6642176400 3121527870 2223664636 3575550356 5576948886
5495002708 5392361710 5502131147 4137441061 3444554419
2101336172 9962856948 9919336918 4729478580 7291560885
1039678195 9429833186 4807560836 7955149663 6448965592
9481878517 8403877332 6247051945 0504198477 4201418394
7731202815 8868457072 9054405751 0601285258 0565947030
4683634459 2652552137 0080687520 0959345360 7316226118
7281739280 7462309468 5367823106 0979215993 6001994623
7993434210 6878134973 4695924646 9752506246 9586169091
7857397659 5199392993 9955675427 1465491045 6860702099
0126068187 0498417807 9173924071 9459963230 6025470790
1774527513 1868099822 8473086076 6536866855 5164677029
1133682756 3107223346 7261137054 9079536583 4538637196

2358563126 1838715677 4118738527 7229225947 4337378569
5538456246 8010139057 2787101651 2966636764 4518724656
5373040244 3684140814 4887329578 4734849000 3019477888
0204603246 6084287535 1848364959 1950828883 2320652212
8104190448 0472479492 9134228495 1970022601 3104300624
1071797150 2793433263 4079959605 3144605323 0488528972
9176598760 1666781193 7932372453 8572096075 8227717848
3361613582 6128962261 1812945592 7462767137 7944875867
5365754486 1407611931 1259585126 5575973457 3015333642
6307679854 4338576171 5333462325 2705720053 0398828949
9034259566 2329757824 8873502925 9166825894 4568946559
9265845476 2694528780 5165017206 7478541788 7982276806
5366506419 1097343452 8878338621 7261562695 8265447820
5672987756 4263253215 9429441803 9943217000 0905426507
6309558846 5895171709 1476074371 3689331946 9090981904
5012903070 9956622662 0303182649 3657336984 1955577696
3787624918 8528656866 0760056602 5605445711 3372868402
0557441603 0837052312 2425872234 3885412317 9481388550
0756893811 2493538631 8635287083 7998456926 1998179452
3364087429 5911807474 5341955142 0351726184 2008455091

7084568236 8200897739 4558426792 1427347756 0879644279
2027083121 5015640634 1341617166 4480698154 8376449157
3900121217 0415478725 9199894382 5364950514 7713793991
4720521952 9079396137 6211072384 9429061635 7604596231
2535060685 3765142311 5349665683 7151166042 2079639446
6621163255 1577290709 7847315627 8277598788 1364919512
5748332879 3771571459 0910648416 4267830994 9723674420
1758622694 0215940792 4480541255 3604313179 9269673915
7542419296 6073123937 6354213923 0617876753 9587114361
0408940996 6089471418 3406983629 9367536262 1545247298
4642137528 9107988438 1306095552 6227208375 1862983706
6787224430 1957937937 8607210725 4277289071 7328548743
7435578196 6511716618 3308811291 2024520404 8682200072
3440350254 4820283425 4187884653 6025915064 4527165770
0044521097 7355858976 2265548494 1621714989 5323834216
0011406295 0718490427 7892585527 4303522139 6835679018
0764060421 3830730877 4460170842 6882722611 7718084266
4333651780 0021719034 4923426426 6292261456 0043373838
6833555534 3453004264 8184739892 1562708609 5650629340
4052649432 4426144566 5921291225 6488935696 5500915430

6426134252 6684725949 1431423939 8845432486 3274618428
4665598533 2312210466 2598901417 1210344608 4271616619
0012571958 7079321756 9698544013 3976220967 4945418540
7118446433 9469901626 9835160784 8924514058 9409463952
6780735457 9700307051 1636825194 8770118976 4002827648
4141605872 0618418529 7189154019 6882532893 0914966534
5753571427 3184820163 8464483249 9037886069 0080727093
2767312758 1966563941 1489617168 3298045513 9729506687
6047409154 2042842999 3541025829 1135022416 9076943166
8574242522 5090269390 3481485645 1303069925 1995904363
8402842926 7412573422 4477655841 7788617173 7265462085
4982944989 4678735092 9581652632 0722589923 6876845701
7823038096 5678831122 8930580914 0572610865 8848458731
0165815116 7533327674 8870148291 6741970151 2559782572
7074064318 0860142814 9024146780 4723275976 8426963393
5773542930 1867394397 1638861176 4209004068 6633988568
4168100387 2389214483 1760701166 8450388721 2364367043
3140911557 3328018297 7988736590 9166596124 0202177855
8854876176 1619893707 9438005666 3364884365 0891448055
7103976521 4696027662 5835990519 8704230017 9465536788

5674302859 7460014378 5483237068 7011900784 9940493091
8919181649 3272597740 3007487968 1484882342 9320230121
2803232746 0392219687 5283405169 0697419425 7614673978
1107154641 8627336909 1584973185 0111839604 8253351874
8438923177 2926135430 2493256289 6371361977 2854566229
2446164449 7284597867 7115741256 7030787188 5109336344
4801496752 4061853656 9532074170 5334867827 5482781541
5561966911 0551014727 9904038689 7220465550 8331707823
9480878599 0501947563 1089841241 4467282186 5459971596
6390156419 4175182093 5932616316 8883801327 5875260146
0507676098 3926257264 1112013528 8591317848 2994756824
7256488553 3357279772 2055435681 2630253574 8216585414
0008053148 2069713726 2149755576 0518904816 2237679041
4926742600 0710459226 9531483518 8137463887 1042735447
6762357793 3993970632 3966049691 4530327388 7874557905
9349377723 2014295480 3345000695 2569809352 8288778371
0670585567 7494813738 5863038576 2823040694 0056653405
8488752700 5308832459 1821834943 1804983419 9639981458
7734358631 1594057044 3683515285 3836094429 5596436067
6090221741 8968835481 3164399743 7764158365 2422346426

1959739045 5450680695 2328507518 6871944906 4767791886
7203064186 3075105351 2149851051 2073138466 4871754751
8382979990 1893177515 5063998101 6466414592 1024068382
9460320853 5554058147 1592732206 7756766921 3664081505
9008069525 4061062853 6408293276 6219319399 3386162383
6069111767 7854482361 2932685819 9965239275 4884274354
1440288453 6455595124 7355461394 0315495209 7397051896
2401579768 3263945063 3230452192 6450496517 3546677569
9295718989 6904709027 3028854494 5416699791 9929480382
5498028594 6029052763 1455803165 1406622917 1223429375
8061439934 8491436210 7993576737 3179489642 5248881372
0435579287 5113858569 7338197608 3524423240 4667780209
4839963994 6684833774 7067254836 1884827300 0648319163
8260221105 5522124673 3323184463 0055044818 4991699662
2087746140 2161570210 2960331858 8727333298 7793525701
8239386124 4026868339 5558706077 5816995439 8469568540
6711744449 3247951957 2159419645 8637361269 1552645757
4786985964 2421765928 9686238350 6370433939 8116713975
4473622862 5506803682 6641355414 4804899772 1373174119
1999700172 9390730335 0869020922 5191244473 9327837615

6321810842 8982077069 7413870705 3266117683 6986477417
8718020272 9412982310 8887968318 8085436732 7806879771
6591116542 2445380662 5861711729 4980382488 7998650406
1563975629 9369628093 5818976149 1017145343 5566595427
5706419440 8833816841 1111662007 5978724413 7082333917
8861147082 2865753107 8536674695 0184621407 3649391736
6254937783 0140743026 6842215033 5117736471 8538723240
4042103790 7750266020 1148149354 8222891666 3640782450
1668153412 1350527857 8539332606 1102498022 7309363674
0213515386 4316930152 6746053606 4351732154 7010914406
5087882363 6764236831 1873909374 6423260902 1646365627
5539768340 1948293279 5750624399 6452725786 2440037598
3422050808 9351290231 2247597064 4105678361 8708771723
3355546548 2598906861 2014101072 2246590400 8553798235
2538851716 2351825651 8482203125 2149507003 7830041121
6212126052 7260599443 2044305627 4522916128 8917668141
6063913123 5975350390 3200775295 8739241247 6451850809
1639114592 9607115634 4204347133 5447209811 7846145107
7872399140 6062902282 7666430926 4900592249 8102910687
5943453385 8330391178 7475759770 6595357097 9640012224

0921990311 5822925966 7913153991 5614380701 2926078019
7022589662 9233681543 1249941225 9460023399 4722281710
5660393187 7226800493 8331489803 3854890946 8685130789
2920642428 1917479586 6199944411 1962087304 9806438500
6852620258 4328420855 8233856693 6649849720 8170461353
7616358401 5342840674 1185875815 4651459827 0228676671
8553093119 2334019128 6170613364 8731831975 6081256946
0089402953 0944291195 9029596856 3923037689 9763274622
8390073545 7144596414 1082292859 2223933283 6210192822
9372435902 8300388444 5701383771 6320565183 5197010011
5722010956 9978904849 6445343461 2129224964 7323561263
2195115570 1565824427 6615993264 6315580667 2053127596
9485380573 6420838491 8887095176 0522878173 3946274764
4656858900 9362661233 1115291081 6041524100 2141959373
4978643166 1556732702 7921095935 4305557973 2660554677
9635520053 7830461954 0636971842 9161685827 3412221714
5885870814 2740902481 8544642177 4876925093 3287856706
7467738122 6752831653 5592452045 7807054135 2576903253
5227389638 4749564625 5940378924 9250076243 8689377647
5310102323 7467337714 7458162553 0698032499 0336764554

3030527456 1512961214 5859444321 5074905149 1453950981
0013887379 2637996487 3728396416 8975551322 7596201183
8248650746 9854920380 9769193260 6437608743 2093856028
1564284975 6549307909 7338541855 8351578940 9814007691
8923890630 9054253488 3896831762 9041202129 4916719581
1935791203 1625143440 9650313283 5216728021 3724159473
4409549831 6138322505 4867081722 2147513842 5166790445
4166173032 0082033090 2895488808 5167972584 9581340713
2180533988 8281393460 4985053234 0472595097 2143314925
8660424851 1405819579 7115641914 5884283300 0525684776
8743059163 9049430687 1343118796 1896374755 0336282093
9949343690 3210319768 9811205559 5369465424 7041733238
9539404603 5325396758 3543953505 1672026164 7961347790
9123279952 6492904515 1148307923 3693821660 1070287265
1938143844 8445326395 1739411013 1152502750 4657493430
6376654186 6128915264 4469262228 8436629946 2732467958
7363835019 3714278647 1398054038 2155134632 2370207153
3134887083 1741465914 9240635949 3020921122 0526103123
9068294134 5696785958 5183934913 8234088427 4312419099
1528708043 3280913299 3078936867 1274139228 9003306999

5875921815 2976124824 0911695158 7789964090 3525773459
3824823205 3055567238 0950222667 9043961423 1852991989
1810655544 1247720450 8510210071 5223523427 9253126693
0108270633 9423217625 7007632313 9159349709 9469332410
1390877916 1651226804 4148097656 1897973504 3151396066
9132583790 3374862083 6695475083 2803187867 0775117752
5663963479 2592197335 7794955549 8655214193 3981702686
3998738834 7010255262 0523123172 1525406257 1636771270
0107609122 8152832650 8984359568 9759610383 7215772683
1170734552 2501941217 0154131879 3651818502 0208773269
0613359218 2000762327 2695032838 2739124382 8198170871
1681089511 8789674670 7073377869 5925655427 1334005232
6706040004 3488434329 0276036049 8027862160 7494696549
8921047444 3927871934 5367017986 7392080384 5633723311
9838558626 3800851634 5597194441 9943446247 6112384461
7615736242 0159350785 2082560060 4101556889 8995017325
5433729807 3561699861 1019084720 9660070832 0280569917
0425901038 7692865833 6557728758 6842504926 9037093426
2028022399 8618034002 1132074219 8642917383 6791762328
2644464575 6330336556 7773748086 4410996914 1827774253

4170109884 3585318933 9175934511 5740238472 9290901546
8559163792 6961968410 0067659839 9744972047 2878818312
0023338329 8030567865 4808714764 6451282426 4478216644
2666167320 9601256479 4514827125 6713266970 6736714461
7795643752 3917429285 0398702258 3734069852 3091904649
6726024341 1270345611 1141498357 8390179349 9713790913
6967064976 3712724846 6613279908 2543054492 9552859493
2793818341 6078270913 2668086565 5921102733 7467001325
8342871524 0835661522 1655749984 3123627828 7106649401
5646701419 4371382386 3454729606 9786933359 7310953712
6499416282 6564637084 9058015153 8205338326 5112895049
3856646875 2921135932 2202656818 5641826082 7538790002
4079158926 4602849089 4922299966 1674377313 4777613415
0965262448 3327093438 9841205692 6145108857 8122491396
1691253420 2918139898 6839013357 9585762443 5194008943
9551805547 4655400005 1766240202 8259448288 3381188638
1749594284 8920135200 9095100786 4941868256 0092739776
6758564259 8378587497 7766695633 5017074857 9027248701
3702642032 8396575634 8010818356 1823721770 8223642318
6591595883 6694873224 1172650448 7268392328 4530109916

7751837683 1599821263 2371238543 5731268120 2445175401
8521326637 4053880290 1249728180 8950215531 0067359818
4430429105 2884593230 6472559044 2355960551 9788393259
3033957293 4663055160 4309237856 7722929353 7208416693
1345752840 1187374685 4691620648 9911647269 0942898297
1065606801 8058078436 0046186622 3562874591 3851859044
1625066322 2249561448 7244138138 4976379710 2676020845
5318241119 6392794106 9619465426 4800067617 2761811563
0063644321 1162248373 7910562361 1358836334 5501022861
7051789044 0570419577 8598333484 6331792190 4494652923
0214692597 5656638996 5893747728 7513933771 0556980245
5757436190 5017724662 1458759237 4418657530 0649980566
8837696422 9825501195 0658378431 2523213530 9371235243
9691496623 1011032824 3570065781 4876772991 6094115395
4063362752 4237129355 4992671348 5031578238 8995675452
8791557842 0483105749 3300601979 5820773955 8522807307
0489509362 3555076983 7881926357 1417793387 5021634439
1014187576 7119389144 1627710960 2859415809 7199134293
1329514592 4373636456 4730350373 7453850348 9286113141
6380947523 0174508878 4885645741 2750033533 0341613809

6560043105 8605483557 7394662503 3230034341 5878146346
0216923507 9216111013 1489482818 9539102891 6816328709
3097131841 3981542767 8818067628 6509780857 1826211700
3140003377 3015815363 3414909323 7034703637 5133545376
3452105037 0995452942 0552320788 1744937093 7677056009
3063536455 1091348162 7378204985 6570556087 8421196403
9972344556 4586076895 1556968689 9384896439 1952252323
0970330103 7277227710 8705649129 6612106149 4072782442
0334140574 4144645996 8236966118 8784116562 9035511783
9944070961 7725671649 1979016819 5234523807 4462998776
6482487375 3313018142 7639105192 3468508197 9001796519
9070504908 6523744284 1652776611 4253515386 6516278131
6090964802 8012344933 7242786693 0894827913 4654439319
6525415482 9494577875 7585994820 9918182452 2449312077
7682508307 6828233500 1597040419 1995605097 0536469647
3142448453 8258881126 0275390954 8852639708 6523390529
4182969180 2357120545 3282318092 7035649174 3371932080
6287313035 8964057087 3779967845 1747405153 1740138487
8082881006 0463889367 1164047775 5985481263 9075047472
9501260941 9990373721 2462016770 3051779035 2952793168

7663050998 3744185980 3498821239 3409198050 5510382153
9827677291 3731380067 1533924012 6954586376 4220650978
1085290763 9079727841 3017645532 4752707378 8764069366
4200121947 4570235829 5481365781 8098679440 2022028082
2637957006 7553935758 0808631893 2075864444 2066446916
4933446769 8180811716 5686652133 8968617359 2450920801
4653125297 7796613719 8695916451 8694323242 4640440167
2381978020 7283944182 6450218313 1483366019 3848919723
1781715437 2192103946 6384737156 3022670180 1343515930
4428538489 4182567887 0721238520 5972638592 2493476362
3122188113 7063075069 1826010968 9069251417 1425142181
5349153212 9077723748 5066354891 7089285076 0234351768
2183550088 2964741065 5814882049 2395337022 7053670563
0750317499 7881870099 8925102017 8015601042 2778362836
4432372977 9929935160 9258845157 7205523289 6978333126
4276712910 9399310377 3425910592 3032776526 6764187484
2441076564 4477670977 9039232495 8416348527 7351719810
6467383714 2742974468 9923204069 3250606283 4468937543
0167878153 2061600905 7693404906 1461766070 9438011091
5443261929 0007452098 9595920115 9412324102 2748454826

0540436187 1836330268 9928586235 8214564387 9695210235
2666733724 3442309157 7183277565 8002119282 7039104239
1966426911 1553335945 6968578281 7020325495 5525288754
6446607462 0294766116 0044355516 0473504429 2127916358
7484735015 9021552212 0388281168 0214138658 6516846456
9964810015 6337412550 9847973013 8656275460 1612792463
5978366148 0163871602 7944054827 1019629077 4543628092
6125675071 8177364174 9763254436 7735036325 8000404291
9906963117 3977878750 8156022736 8824967077 6355598692
8490162876 8699628053 7901818481 4881083394 6900016380
7910759607 4550468891 2686792812 3911488800 3672072973
0801354431 3253477130 9418671717 8607522981 3735391267
7281259395 8220524289 9913716906 8565042157 5056729991
2741771492 7960883150 2358697816 1908949084 8771772250
3860872618 3849479397 5744066491 2760518878 1242336831
2546727833 1513186758 9156683006 7921021594 7336858591
2013953603 0167811041 3444411030 9033887615 2048829690
9104689167 6715553733 4662254557 5975202624 7712427962
2598327840 5833585897 6714742057 2404743972 0232895903
7261486883 8800317414 6490203843 5903585279 9312387104

2845981608 9961019456 9164698383 7718267264 6852648691
7294841415 3004604004 2995850351 6410189902 7529366867
4318349554 4745812414 0190754681 6077709779 2057938389
5378192128 8474099295 3704054696 2226547278 8072486855
0804657104 3123854873 3516530705 7078458424 3335550958
2219128627 9720545546 6267099131 9023703117 7969089278
6623112661 3376711785 1294305932 3281605826 5356238481
6419214473 2543731002 0627384668 1235169101 6359252588
2568064389 4638988087 2735284406 4622081495 1386227523
9938938734 9050826254 7241778170 2582044129 8537604998
2789902008 3498387362 9924981257 4235456843 9023012261
7336658205 4678567114 7973065077 0354756205 6742830018
7473019197 3108811575 1677700507 1432012726 3546019124
6080045160 8108641835 5396699469 3694732227 1670748972
8504641953 9296643472 5254724357 6591929699 4906167018
9061433616 9070561482 8098036324 3454128229 9682759802
2669404564 2181328624 5175496521 4722162083 9824594576
6133427105 6495719356 4431561774 5008283769 3570099541
9541839029 1510331879 3390761420 7467028867 9685949854
3978945730 0768939890 0700739246 9746181285 5764662265

4129132040 5227907121 2820653775 0582800408 9716346716
3709024906 7747363091 3690400261 5646432159 5609108510
9244516245 4420141442 6416601813 8599001741 7408244245
3786101584 3336177729 2580611159 1920084140 9188819120
8858207627 0114836717 6074904698 0914443057 2622111045
8330078933 1698191603 9171506227 9298628270 9446275915
0096832263 4507372545 1366858172 4834984700 8084016386
8209726371 3452054398 0227786633 7293290829 9140106455
8976169745 5978409211 4091676840 2026937022 9231743334
4999869018 4151088899 3165125090 0011637191 1499485202
4821586396 2162949817 5309462304 7604832399 3793910021
4253299647 6235163569 0094450860 5809120245 9904612118
6233182786 1446472779 5523218635 9165518830 5793065770
3331498510 0683571356 2434188188 4405780028 8440181290
3137865379 4869614630 4677269145 5295369015 4167025838
0324778422 7241799451 3653582260 9716525883 5671213351
9546838335 3498015032 6935979816 7463231847 6283063405
8832473122 8951257944 2676398779 4671312104 2763380872
6957386093 1463153914 8548792514 0288850251 8978807602
3838995615 6848503919 9585502925 6054176767 6631453540

5849629679 6781349420 1160033258 7443143874 6248313850
2149804016 8194079568 7219268462 6172874034 8096793194
9965604299 1902818105 9760326325 1746405016 4546062667
6552901063 9868703668 2632990505 7770626639 7868453584
3840576732 9826816344 8646707439 9909175040 1889231926
7557518354 0549560177 3290712721 9134577524 9057715127
7335842331 4008356080 9269622988 9416304728 7780054743
7984985455 6287072996 8407382937 2186238317 6652471609
0967192007 2376588942 2618655048 7552614557 8558987730
0870323472 6418384831 0403948187 4361622445 5286163287
6285411759 4646049702 7724490799 2751464457 9298254980
2258601001 7724378401 6772316680 2004162547 2441794155
4781055417 8036773553 3544670303 2646961944 7560812831
9330956796 8558277193 2031205941 6166939020 4966535218
9672822671 9726400294 9330738471 7544753761 9370178829
7638248723 3361813499 4145416947 3654925484 0633793674
3615410815 9346496043 1603544354 7377288023 6104774311
5330785159 9029777714 9961027462 7769759612 4888794486
0986334942 2852847651 3102779262 7974398195 7617505591
3009933773 6824051090 2583759345 1700153405 2226614407

7237050890 0444966132 9585953602 0556034009 4928209438
6299461883 4790932894 1610988565 9495421311 4335608810
2394237060 8710802646 5913203560 1218759337 9163966643
7282836752 3283916888 6537375133 5794859860 1075693748
8964565718 7292540448 5086244499 4781627384 2517229343
9601372124 0628678363 6675845331 9047439547 4066401526
0871940915 7439552827 7390430386 8772728262 0656631293
8745987531 7749973799 2930432943 7176380185 6280061141
6195639424 1431225439 7099163565 1028483157 6542703790
6837175764 8702300523 8819749874 6636856292 6550582228
8771322178 1440489538 0996810721 4301239469 3530931524
0540812157 0540227441 4521876541 9014283867 4426001188
9041724570 5374707555 5058163283 1687247110 2203537271
6611230485 7340460879 2725016947 0106783117 8927095527
2532221252 2436167334 3366384756 5909497282 2180941868
4074238351 5678688934 2114820390 5824224324 2646436302
0144178798 2022116248 4716574682 9114631540 7563770222
7401358411 0907607846 4780070182 7663362279 7810454633
1131294044 8335701348 6958516526 7459515187 6800333955
2241054818 1767867772 1527982702 5011719581 6577603549

7329237247 3206785369 0257536233 9712168843 9087887926
2188202305 5299371323 9719433308 3536231248 8703864161
9436150652 9551267334 2071985022 5977140863 8122015980
8943635618 0859701008 0081622557 4550391013 2198197904
5520049618 5837777210 4804663553 3806616517 0235950971
3320363157 8945644487 8009456203 6978497345 9902004606
8865727018 6586775784 2758530645 7066171271 9496737108
3950603267 5015324359 0902949151 6973738110 8979347822
9768410011 7657987098 1857251313 7226774970 6609250481
8768355160 0371463868 5918913011 7368052187 4326542606
3700710595 3644250627 6045825233 6880552521 1815664175
5343068118 1548267844 1693152844 0846108758 8214317641
6498356631 2751872818 2948655658 5242068522 2183075530
6118393326 9341644594 1534265177 8653397980 5808281588
0630074995 2897558204 6866125908 5367873860 3318442905
5106897786 9841773560 3118111677 5638725899 1151680323
6547002987 9896289861 8101459647 1307916144 3695646909
0951878857 4398821730 5838849808 0952307756 9358851616
0277195214 8899835863 2323127308 9098615607 7738600698
4035267826 7853872159 2093625581 7889813416 2474864564

3321104319 4821421299 7931881046 3639954149 6539441501
3838687483 8487022468 1829391860 3195986679 6236348930
9283087840 7124004310 2270613759 1368056518 8613134583
0799070500 3607588327 2488678793 2409338007 1864152853
3179435350 7340189119 3638546730 0006604537 8378447246
9288830546 9790001312 4895210044 6949032058 8382949236
1391928430 5249167833 0129801922 5515705037 8521810552
9616236375 2364796268 5751660066 5393641422 7306300164
8652613891 8422435017 9745599361 6794063303 5221118290
7159753882 1839777552 8129815385 7016870220 2620274678
6479166440 3072901844 5497956399 8448368078 5199708820
1407769199 2616749911 4832982185 4382718946 2821653870
6485858864 6221611410 3435703428 7886297908 3418871606
2144300145 3327502971 5104673156 0210000438 6951058377
3779766003 4608876248 6164093864 5252177935 2899475784
9625524392 5598620521 4090523462 5084783048 7046492688
3132894705 5389135729 0706967599 5562985866 6955972168
6506052072 8013421043 5576277918 4021797626 6564845802
6159140717 3477009039 4751680177 0990012939 1137881248
5342559493 1286665346 5033728846 3906499684 6064474190

7524313323 9034049081 9523304438 9559060547 8549546202
6325667681 3262435925 0202495162 7560708090 0436460421
4970256914 8855526502 2810327762 1158422824 3326952862
9137662675 4819935461 1814391336 7579700141 2558701433
1943476403 5725376914 3888996830 8826284461 6425575034
0014289825 5762038636 4384137906 5196129177 7735418369
4676232982 9049812617 1767619155 4292570438 4322399184
8226174435 0470199171 2582146876 8317264607 8959690569
9813532644 3597396517 3473319484 7987580641 3792688541
3552523275 7204573294 7721570685 0016950046 9597583893
7352753862 2664943456 4370716105 1152161717 6237598050
9005532321 5489606281 7794302268 6405795558 4573060059
8376482703 3398594200 9858235140 0179507104 5690191913
5906230410 2336798080 9072401963 1267526891 6362136351
0326480772 3291495085 9151265812 1438233710 7294914808
8472355286 3941959934 5568415634 4577951727 0333742381
2990326019 8160571971 1839506627 5822032183 7136059718
0259408706 1553471310 4482272716 8483955241 0591360591
9812444978 4581108545 1123166817 3534838253 7248253476
3677758171 2867205865 1482853172 7356906983 9935110763

4320913197 8031403165 8897379628 3011784098 0641017501
6511072932 9078321774 8756628931 0650383806 0933728413
9922673338 4778203302 0207005171 8894170646 5146238366
7206327426 4433661217 4011766914 9192355709 0564480301
6342294301 8376552631 0845017251 0307540942 6044096870
6628806626 5900569082 4514076325 9915816449 9361455172
4520570204 4309372230 5550217222 2997062097 4926860976
2787409626 4487720560 4307863480 8885709143 4647932415
3621430319 9965695610 7535704172 0728533425 0171325558
8181132955 0409521783 0139465216 4365942629 6076857058
5698507157 1513172629 2896007258 7601564840 5560886131
6541183595 8628710665 4962825995 3512719324 4635791046
5543891651 5095418730 6071015034 4306095823 0225745597
4944275067 6309263225 2996633821 9395202927 9179732470
9455969101 6402983683 0804263099 1048156750 3623509654
9243025895 7527352141 2445149542 4629722585 1012070780
2110188106 7223479725 7933065318 7713438466 7138075463
8347163542 8854957610 9428418986 0179465872 1444495198
8015508040 4250645219 1484989920 4000073106 7236994465
5246020908 7678823000 6433772565 7385010969 8990581912

9095707986 6699453765 0804079178 5243822204 1070599278
8892677457 5208428752 6377986730 3605612307 1072392258
1504781379 1727312612 3487833403 4473833573 6019732359
4660427370 4635201327 1825924109 0604009763 8585857716
9584195631 0957774852 9579836844 7568031218 7481820283
3941887076 3117316152 8981175642 9711334181 4972180780
4046507765 7204457082 8594174751 1492617936 7379999220
1817893994 3333773114 6911970737 8610419639 8642216604
5588965683 2067013375 0574503887 2111332436 7398402841
8863914763 3491695114 0325834758 4151417032 5690161784
9314557069 0416985805 0217798497 6370147589 1481054320
5854914100 6622017217 1972687893 0012101267 4812702359
4085516260 1689425111 4584996583 1558966046 0091525797
8816703846 2590538325 6920520425 7913789488 2757960327
8877535466 8614418268 2779765125 8953563761 4859944850
4970663840 6266121957 1419110632 4606177418 0577212381
6598724724 3225296909 8533628440 7990300075 9454628154
9235506086 4815579289 6196961706 0715201589 8252997728
0352000261 0888814176 5066362169 0592802151 6429198484
0774461436 1789141519 1517976537 8482826870 1875003026

4867608433 2046585254 7055588241 0254654806 0404373727
7183476901 4720664234 4343742555 1412917850 3032471263
4180765251 8780292553 4774001104 8539969605 4992650809
3910691337 6148418348 8459636562 1526610332 2394174670
6436834050 4749943339 8022856103 1308303848 4571294767
3898562939 3764191440 7036507544 6220611864 9912724964
3799875806 5378502037 5318997261 8014404667 7930501403
0158070926 6213229273 6497186539 5286656753 8572115133
6061144572 2280085118 3757899219 5430634136 9230229313
9751143702 4048302273 5762903991 1794499248 4809150710
0244407848 2866598579 4065255391 4104149734 2780203520
1354199259 7762817818 2825372022 9201081864 4944834925
5421793982 7232793570 9582874859 7126780783 1342861807
5049717574 7373730296 2804773769 0893255891 4598141724
8526582995 1088223005 5223242218 5861913947 9518422013
1553319634 3639226842 5916416866 9438122537 1359607100
3174365195 9027712571 6045884860 4482067441 0935215327
9068160320 5421596795 9066411120 1876185312 5671015021
2239401285 6686084694 3593740815 8536481912 5280049207
2404217217 0913983123 1180540432 7701583562 9513656274

6102488277 0648886503 7765175678 8068724988 6165709484
6665770674 5770002071 4433252555 5736557083 1503200190
8299209654 5498737419 7566086195 3349231294 0263904930
9820147003 7116182948 5939931199 9550704553 8119671128
9367735249 9581820117 7479978863 6393286405 8078108186
5733766815 7893827656 4506429173 9668557955 5053188715
3145523530 7035599474 0186225988 1498546607 3778769878
1542360397 0809774123 6151824596 4026869979 6095645238
2858423595 3564615185 4481657999 6646064826 1396618720
3048391195 6025038111 1550938420 2098945915 5576008389
7989949964 5662625405 1419561078 0090298667 0146352385
3206603257 4466820259 4306188017 7309110921 2741138269
1487843556 7935257280 8875543164 6930772353 6376822603
6080174040 6609971511 7688043492 7489197133 0878229511
2374663263 5635328517 3941894665 1094374576 8270782209
9284680346 8415744312 7739811044 1867620329 5447546807
7511126663 6854799444 6093480999 2951875666 4999022616
8601967205 3749149951 2268236378 9586524546 2813439289
3383651565 3699241310 9638102559 1146439238 0521390786
2893561660 9988364791 7563317672 5856523591 0695203268

9599005488 4753424160 5866898200 6748316317 4286329119
6333991327 0908606507 4595260357 1573230697 1210642342
4081597068 3287076244 3716553275 0228797802 5986909811
1122655888 8151520837 4824500344 6304650598 4569690276
1669582789 8291361353 5306291331 4278818882 4934213644
2417833519 3197865439 4020146532 8083410341 7852724898
7905091993 2369270996 5671335077 1190589994 5951923990
6151561654 8030014535 9212550696 4053452638 2345215599
9210578191 3710301889 7920640888 3974767667 1447273142
5446792350 0524618849 2374553075 7573490270 7342496298
8799969420 9459596100 8702501329 4533253580 4568928570
7241207965 9198092255 5056006197 1283541270 2020725839
9417117552 0920820151 0965095266 8511389757 7150810849
4435082854 5874991294 3857563115 6683245668 2799299186
1539009255 8717168404 9566399195 9154034218 3645372120
2367860865 5364745175 6548793189 2564408527 4489190918
1934116675 8356343975 8886046349 4131118752 4103842546
7937999203 5469104119 3544311321 9136068129 6575685836
1177456465 4674861061 9885914148 0579931872 5367531243
4703354826 3752708135 3105570818 0496424985 8464614797

3467599315 9465147870 2506527108 3508782350 6565323317
9773865666 6181652390 0176649884 8545605496 1300215776
1152558133 9618402706 7814900350 2528768236 0782210739
7102339146 8701597358 6858901529 7010347780 5032921540
1435959529 8683404657 4717562321 9664051540 1477953167
4617262087 2730482063 4652469109 9533273755 6109057837
8455945469 1602236876 8964142596 0164689647 1063480741
0992854648 2353083540 1323329248 6403731800 3195202317
4762065377 2616371744 5360549726 6906017111 7676104777
4971666890 1521638389 7431171418 0622222345 7185679415
0729952620 1086205084 7831274747 9190999688 9937275229
0536747850 2050003863 0036526218 8006709266 7410480602
7341997756 6600294279 4109040006 4654281074 4540076164
2952536246 0261476180 4717443228 8995328582 8397762184
6009676692 6758127030 2806519535 4520531735 3680895458
9902180783 1457758912 8020397005 3633193821 1000954432
4124419794 9192916205 2344213463 9565384078 1209416214
8350011558 8361842116 4283992454 0275907196 2153757018
7067083731 0122461413 6204892655 5668109467 0763865360
8301584761 4512581588 5696100303 3708119705 8344452874

6661988915 3466424488 7911940711 4239401159 8697079574
5946337170 2432684848 6463201898 6352827092 3130470892
1568475820 7753034387 6899787023 2343858438 1125011714
0132657693 2055491186 0153519551 6546279411 7559396794
7958810333 9354132897 0252889353 3748106257 8756203642
9427025751 2121137330 2138119513 9575641912 2685155962
4762032820 3872634206 6227347868 2230365220 1965572932
5905068134 8492922996 4724822935 9787842720 9455782673
2997585381 8536442370 6173535176 5306039680 1087899490
5066544915 4457795216 6038552398 0137981043 4056418240
3396162494 9104547121 0483943920 0945914647 5424247859
9109690004 6541371091 6300967859 5156394733 2190934511
8386699646 2278885581 7353221326 8766349580 5912376125
1203010983 8678411957 2588779920 6041260049 8658950272
4713314676 3722204388 3985583477 7011259942 4691208308
5956667875 3194246513 1444389971 1959681059 3795753215
5524204659 4100814183 5112017419 6853432672 3432718680
9962504543 2475688702 0553419691 9954530095 2644398446
3843465988 3041826293 2239295612 6100458846 4424428501
1551557765 9357803795 6502680613 0721758672 0485417971

5789640155 4276881090 4758995646 0548836298 9140226580
0261341580 3948035797 1019004151 5476550183 9175577267
7897148793 4773727475 2574389815 8705040701 9682151012
1882608804 0084551332 7951628412 8067967896 5570163917
0677798415 2914939740 3158167896 8654488413 1904636833
2179115059 1078138982 6102627197 9696826411 1799186560
3899389541 8928488851 7501225047 5477899950 8544083983
8007254314 6884298841 2616042682 2488230977 8855649576
5424017114 5103939279 8029099760 4904428832 1989767513
2053511523 0545666467 1437959319 1527268027 8210241540
6297958288 2846635562 3580986725 6382005652 1551995179
3551069127 7105385526 6192690352 6081367717 6664350712
1345398371 1357500975 8544059395 5866173782 8297120544
6931822604 01

第1万項まで計算して小数第35,662位まで計算できました。計算に80秒ほどかかっていますが、タブレットPCでバッテリーモードで動かしてこれですので、もうちょい良い環境だともう少し速いでしょう。それにしても分母に階乗が入っているだけあって、収束はめちゃめちゃ速いですね。

あとは、FractionsライブラリがGeneric Math関係のインターフェースを実装してくれたらあえて今回のように別実装する必要もなくなりますね。期待しています。

2022年6月19日日曜日

Seeed XIAO BLEの充電回路を使いこなす

最近、国内の各電子部品店でSeeed XIAO BLEという製品が取り扱われるようになりました。親指サイズのデバイスで、nRF52840というARM Cortex-M4 CPUを持ったマイコンモジュールで、Bluetooth 5.0に対応しています。もちろん技適も通っています。

末尾に"Sense"が付いたモデルと付いていないモデルの2種類があります。Sense付きは700円程度高くなりますが、6軸IMUとPDMマイクを搭載しているようです。それ以外は同じです。

さて、特記すべきことに、このデバイスにはBQ25101というTexas Instrument製のリチウムイオン電池充電コントローラーが付いています。そのため、適当なリチウムポリマー電池などを直結して、充電式のBluetoothデバイスなんかを簡単に開発することができます。なんというかすごい時代ですね。本当にIoTが個人レベルで作りやすくなったと感じます。

ハードウェア

回路全体

さて、一応Seeed wikiにこのマイコンボードの回路図は載っているのですが、非常に見にくくてわかりずらいです。そのうえ、現物との対応もあまりとれません。ということでいろいろひも解いてみました。

結論から言うと、回路は以下のようになっています(電源にかかわらない回路は省略しています)。

点線で書いた部分のみがこのボードの外の配線となります。ですので、バッテリーを基板の裏面にある「BAT+」「BAT-」端子に接続するだけです。裏面のSMD端子なのが少し不便ですが、まあそれはよいでしょう。

Seeed wikiより引用)

ただし、回路図から見てわかるように、Seeed XIAO BLEに搭載されているのは充電コントローラーだけで、過放電保護回路は付いていません。電子工作で手に入るリチウムポリマー電池は保護回路付のものも多いですが、18650サイズのリチウムイオン電池など、保護回路が付いていないものはDW01/FS8205などの過充放電保護ICなどを組み込むとよいでしょう。詳しくはこちらの記事を参照してください。

ちなみに、Q1はなぜダイオードではなくFETを付けているのかと思う人もいるかもしれませんが、これはダイオードでの約0.6Vの損失をなくすためです。FETだとダイオードに比べてほとんど電圧降下しませんので、3.7Vのリチウムイオン電池で駆動する際も、3.3V以上の電圧をレギュレーターに供給できるようになります。

充電コントロール回路

さて、メインディッシュは充電コントローラーのBQ25101となります。 とはいっても、BQ25101はよくあるタイプの充電コントローラーで、電圧がある一定(約4.2V)になるまでは定電流充電を行い、そこから先は定電圧充電を行うタイプのものです。電池の温度制御(温度が高くなりすぎないように電流を抑える機能)もあるようですが、今回は殺されている(TS端子に温度センサではなくただの抵抗が取り付けられている)ので機能しません。

充電プロファイル(データシートより引用)

ISET端子は充電電流を決める端子で、その抵抗値により決まります。範囲は13.5kΩ(10mA)から0.54kΩ(250mA)です。このボードではR18に2.7kΩが付いているので、デフォルトで50mA流れます。また、マイコンからP0.13をLOW出力してあげればさらに2.7kΩが並列につながるため、充電電流が50mA増えて合計100mAを流すことができます。

~CHG端子は充電状態か否かを示す端子です。オープンドレイン端子となっていて、LOW(=FET ON)で充電状態、Open(=FET OFF)で充電完了状態を意味します。充電LEDがこの回路に付いているのでマイコンの制御なしにそのままLEDが機能しますが、マイコン側からP0.17の値を読み込むことで充電中か否かを判断することもできます。

さらに、R16とR17で分圧されたバッテリー電圧をアナログピンで読み込む回路も一緒に入っています。これによってバッテリー電圧をマイコンで知ることができます。もちろんこの回路を有効にするにはP0.14をLOW出力する必要があります。普段はこのポートを入力(ハイインピーダンス)にしておくことで、R16-R17の計1.5MΩを流れる非常に微量の電流(約2uA)ではありますが節電することができます。

なお、このマイコンボードではバッテリー電圧を測ることしかできないため、例えば負荷が大きく上下するような環境でこの充電回路を使用した場合、負荷が上がれば(=電流が大きくなれば)バッテリーの内部抵抗での電圧降下が大きくなるためバッテリー電圧は下がり、その後負荷が下がったときにバッテリーの電圧は回復します。そのような場合は、バッテリー電圧しか見ていないと正しく電池の消耗状態を把握することができません。もっと厳密に電池残量を測るには、クーロンカウンタと呼ばれる電荷の移動量を監視するICが必要になってきますが、このボードにはそのようなものは無いため、外付けしない限りはあまり厳密な電池残量の把握はできません。電圧はあくまでも参考程度というものになります。

ソフトウェア

さて、充電回路がわかったところでソフトを作っていきましょう。今回はサンプルでBluetoothキーボードとして使えるデバイスを作っていきます。

Bluetooth 4.0から規格が制定され、もちろんこのSeeed XIAO BLEでも対応しているBLE(Bluetooth Low Energy)にはBattery Serviceというものがあり、ホスト側にバッテリー残量を知らせることができます。例えばWindows11だと、Battery Serviceに対応しているデバイスでは以下のように電池残量を示すアイコンが出てきて、極端にバッテリー残量が低下しているなどの場合は通知で警告してくるなどの機能があります。

このSeeed XIAO BLEはせっかくここまでの充電回路を持っているデバイスなので、このBattery Serviceを使いこなしていきましょう。

環境構築

さて、今回はArduinoで開発していきます。

Seeed Wikiにも書いてある通り、Arduinoの「追加のボードマネージャのURL」に "https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json" を追加して「Seeed nRF52 Boards」をインストールするのですが、ここで注意点があります。バージョン1.0.0をインストールしてください。デフォルトだと最新?の2.6.1?が出てきますが、ここで今回使うライブラリは1.0.0しか対応していないようなので、1.0.0を入れる必要があります。

あとは通常通りでOKです。ボード選択などを適宜して使える状態にすれば準備完了です。もしわからなくても、ググればそれなりに情報は出てくるはずです。

setupの実装

早速実装していきましょう。 Adafruitのライブラリを使用していきますが、特段ライブラリをインストールせずとも使えるはずです。

#include <bluefruit.h>

#define	PIN_HICHG	22
#define	PIN_INVCHG	23

BLEDis bledis;
BLEHidAdafruit blehid;
BLEBas blebas;

ヘッダーはこんな感じです。なぜかHICHG(100mA充電)端子のピンと~CHGのピンがヘッダファイルで定義されていないのでここで定義しておきます。

サービスは3つ使い、DIS (Device Information Service)、HID (Human Interface Service)、BAS (Battery Service)です。DISはデバイス情報を伝えるサービス、HIDはキーボード動作そのもののサービスです。

void setup() 
{
	setup_battery();

	Serial.begin(115200);

	setup_ble();
}

void setup_battery()
{
	// High speed charging (100mA)
	pinMode(PIN_HICHG, OUTPUT);
	digitalWrite(PIN_HICHG, LOW);

	pinMode(PIN_INVCHG, INPUT);
}

void setup_ble(void) 
{
	Bluefruit.autoConnLed(false);
	Bluefruit.begin();
	Bluefruit.setTxPower(4);  // Check bluefruit.h for supported values
	Bluefruit.setName("nRF52840 Keyboard");

	// Configure and Start Device Information Service
	bledis.setManufacturer("EH500_Kintarou");
	bledis.setModel("nRF52840");
	bledis.begin();

	blehid.begin();
	blebas.begin();

	// Advertising packet
	Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
	Bluefruit.Advertising.addTxPower();
	Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD);

	// Include BLE HID service
	Bluefruit.Advertising.addService(blehid);

	// Include BLE battery service
	Bluefruit.Advertising.addService(blebas);

	// There is enough room for the dev name in the advertising packet
	Bluefruit.Advertising.addName();

	Bluefruit.Advertising.restartOnDisconnect(true);
	Bluefruit.Advertising.setInterval(32, 244);  // in unit of 0.625 ms
	Bluefruit.Advertising.setFastTimeout(30);    // number of seconds in fast mode
	Bluefruit.Advertising.start(0);  // 0 = Don't stop advertising after n seconds
}

Setupルーチンはこんな感じでよいでしょう。まずは100mA充電を有効化し、その後にBLEのセットアップをします。各Serviceを起動して、最後はアドバタイジングの設定をすればOKです。

バッテリー残量

さて、先ほど出てきた問題です。loopルーチンを実装するにあたって、バッテリー残量をバッテリー電圧から推定しなければなりません。ということで、マイコンからバッテリー電圧を見たらどのように見えるのかを確認してみました。使用したバッテリーはaitendoで売っている300mAhのリチウムポリマー電池です。なお購入してから数年間経っているため、性能が落ちているかもしれませんがそこはご容赦ください。

まずは充電です。電池を過放電防止保護がかかるまですっからかんにしてからSeeed XIAO BLEの100mA設定で充電を開始しました。

1時間45分くらいかけて4.3Vくらいまで電圧が上がって、その先は定電圧モードになっています。4時間くらいのところで充電が終了してその拍子に少し電圧が下がっているのが見て取れます。ちなみに、これはこのマイコンボードのA/D変換で測った電圧で、4.3Vくらいまで上がっていますが、手元のテスターで測るとピッタリ4.2Vでした。抵抗の誤差とかなのかなという感じです。

続いて放電です。充電が終わってから過放電保護がかかるまで電子負荷装置で100mAで放電をしました。

放電はこんな感じです。4.1Vくらいからほとんど一定に電圧が下がっていき、3.6Vくらいからガクンと落ちています。

loopの実装

さて、こんな感じでどうでしょう。

#define BAT_AVERAGE_COUNT	16
#define BAT_AVERAGE_MASK	0x0F

void loop()
{
	loop_led();
	loop_battery();
}

void loop_led()
{
	digitalWrite(LED_RED, HIGH);
	digitalWrite(LED_GREEN, HIGH);

	uint32_t ms = millis();

	if(Bluefruit.connected()) {
		uint32_t interval = ms % 3000;
		digitalWrite(LED_BLUE, (interval < 100) ? LOW : HIGH);
	} else {
		uint32_t interval = ms % 2000;
		digitalWrite(LED_BLUE, (interval < 100 || (interval >= 200 && interval < 300)) ? LOW : HIGH);
	}
}

void loop_battery()
{
	static uint32_t lastMeasure = 0;
	static bool lastIsCharging = false;
	uint32_t ms = millis();
	bool isCharging = battery_isCharging();
	static uint16_t rawvalues[BAT_AVERAGE_COUNT];
	static uint8_t index = 0;
	static uint8_t count = 0;
	
	if(ms - lastMeasure > 3000) {
		if(lastIsCharging != isCharging) {	// 充電状態が変わったらリセット
			index = 0;
			count = 0;
		}

		pinMode(VBAT_ENABLE, OUTPUT);
		digitalWrite(VBAT_ENABLE, LOW);
		rawvalues[index] = (uint16_t)analogRead(PIN_VBAT);
		pinMode(VBAT_ENABLE, INPUT);

		index = (index + 1) & BAT_AVERAGE_MASK;
		count = min(count + 1, BAT_AVERAGE_COUNT);
		uint16_t rawtotal = 0;
		for(int i = 0; i < count; i++)
			rawtotal += rawvalues[i];
		
		double volt = (double)rawtotal / count / 1024 * 3.6 / 510 * 1510;	// 10bit, Vref=3.6V, 分圧比1000:510

		if(isCharging) {
			if(volt <= 3.78)
				blebas.notify(1);
			else if(volt <= 4.02)
				blebas.notify(3);
			else if(volt <= 4.25)
				blebas.notify((uint8_t)((volt - 4) * 140 / 5 + 0.5) * 5);	// 4.25Vで35%になるよう5%単位
			else
				blebas.notify(70);	// ここからは定電圧領域になるので電圧じゃほとんどわからない
		} else {
			if(volt <= 3.5)
				blebas.notify(1);
			else if(volt <= 3.62)
				blebas.notify(3);
			else if(volt <= 3.8)
				blebas.notify((uint8_t)(((volt - 3.62) * 277.78 + 5) / 5 + 0.5) * 5);	// 3.8Vで55%、3.62Vで5%
			else if(volt <= 4.1)
				blebas.notify((uint8_t)(((volt - 3.8) * 150 + 55) / 5 + 0.5) * 5);	// 4.1Vで100%、3.8Vで55%
			else
				blebas.notify(100);
		}

		lastMeasure = ms;
		lastIsCharging = isCharging;
	}
}

bool battery_isCharging()
{
	return digitalRead(PIN_INVCHG) == LOW;
}

バッテリーは上の測定結果をもとにパーセンテージを表示するようにしています。しかし、先述の通り電圧によるバッテリー残量測定は目安程度にしかなりませんし、とくに充電なんかは定電圧領域に入ると電圧で判別することはほとんどできなくなるので70%固定とかいう雑な実装をしています。もう少し頑張りたかったら経過時間なども考慮に入れてみるのもいいかもしれません。

また、充放電以外にも接続前は青色LEDが2回点滅、接続後は1回点滅にしたかったのでそういう処理も入っています。

これでだいたいSeeed XIAO BLEをリチウムポリマー電池で使う準備が整いました。やりたいことの実装へ移っていきましょう。