Raspberry Pi 1をNanoPi NEOに置き換える

(えっと、久々の更新ですが、Buildのお話は皆さんブログとか書いているようなのでお任せして、デバイス系のネタです)

かなり長期に渡って運用していたRaspberry Pi 1が気になっていて、特に異常は発生していなかったのですが、そろそろ更新しようかなと思いました。

実物はこれ:

RS componentsのケースに入れていました。RPiなので、GPIO繋いでIoTっぽい何かをするみたいな有用な使い方ではなく、完全にUnixマシンとしてDHCPとDNSサーバーの運用をしていました。RPiで運用していた理由として:

  • 入手が容易であること(壊れたりしたときにサクッと入れ替えられる)
  • 低消費電力
  • 有線LAN
  • USB(UPSのプライマリ受信)
  • 扱いが堅牢である(ファンNG、つまり発熱があまりないこと。また、ボードむき出しとか不可)

という辺りで丁度ミートしていました。もちろん、動かす用途がDHCPとDNSサーバーなので、スペックもそれほど高い必要はないです。DHCPとDNSは他のマシンと兼用させることも不可能ではない(例えばNASにやらせる)のですが、経験的にそれらがあぼーんした時に面倒な事態に陥りがちだったので、独立して堅牢に動作することを求めています。

(もう少しいうと、static A recordの管理と、DHCPによる自動レコード更新をやりたいというのが背景にあります。それがなければルーターの代理応答だけでいい。ところで最初の1年ぐらいは、今は亡きcallweaver入れて、SIPサーバーとしてフレッツ光回線の電話側を収容していました。パフォーマンス的にはギリギリ、ちょっとプツプツノイズが入る感じでした)

しかし、もう忘れていましたが、RPi 1もそろそろ10年選手になるわけで、流石にちょっと心配です。不安の最大要因は、意外と発熱があることでした。写真にあるようにヒートシンクを付けておきましたが、長期間の運用では温度が高いとコンデンサーの寿命を削っていきます。そんなわけで、壊れていないけどもう入れ替えようと言うわけです。

そうそう、Unix、とお茶を濁す書き方をしましたが、これはRaspbianではなくNetBSDを動かしていました。NetBSD、管理系はシンプルで扱いやすいんですよね。私の出自もBSD系ではあるので。しかし新興ハードに対して選択肢が厳しいのも事実なので、Linux系への移行も前提とすることにしました。

VoCore2 Ultimate

で、最近だとRPi4が出ているわけですが、上記の点でもう選択肢から外れました。RPi ZeroシリーズにはLANがないのでやっぱりダメ。それで、他の選択肢を探す事にしました。ググって初めに目についたのは、VoCoreのVoCore2 Ultimateです。

ちっさ!!! ブレイクアウトボックスじゃないですよコレ…

しかし、小さすぎるのも扱いにくい要因になるので、一応ざっと仕様を眺めたのですが、CPUはMediaTekのMT7628でMIPS系、クロックは580MHzとかなり低いので、消費電力は期待できそうです。WiFiも載っているようですが、今回は使わない(&TELなんとか)ので問題はない。有線LAN 100MとUSBがあるので、I/Oの問題はなさそう。

あとはOSですが、LinuxベースのOpenWRTのようです。OpenWRTは使う機会がなかなか訪れないためまだ試していませんが、DHCP&DNSの運用なら却って面倒がないのかもしれません。但し、RPiのDebianやUbuntuのように、既存知識は活かせない可能性があります。

まあ、考えてても分からない、そこまで高くない、実際に来るとなると時間が掛かると思われたので、それ以上悶々とせずにポチりました。

NanoPi NEO

おい、VoCoreはどうなったんだとツッコまれそうですが、実はaliexpressを眺めていたら、こういう写真に出くわしたのです:

そういえば、昔見たことあるな、と。で、改めて調べてみると、FriendlyELECNanoPiシリーズでした。このシリーズは結構沢山出していて、RPiっぽいフォームファクタやら、豪華なインターフェイスがついているものや、VoCoreのように最小限に絞ったものまで色々ありました。

その中で写真のデバイスは、NanoPi NEO2にOLEDディスプレイとボタンを付けた、NanoPi NEO2 Metal Complete Kitという物のようです。

(なお、最初にaliexpressの写真上げましたが、劣化コピー品掴まされてトラブルに合わないように、本家から買いましょう)


このOLEDとボタンというのは、微妙にCobalt Qubeを思い出してちょっと欲しくなるんですが、どうせ今回の用途で使うことはないので、破損箇所を減らす意味でももっとミニマムなやつでいいと、探したのがこのNanoPi NEOです:

これも大きさという点ではVoCoreに毛が生えたぐらいの小ささですが、堅牢なアルミケースに入っているというのが気に入りました。なお、アルミケースということもあり、WiFiは使えません。他のラインナップではWiFiを内蔵しているものもあるので、気になったら探してみると良いと思います(RPiとの差別化が難しいかもしれませんが)。

あと、基本的な仕様としては、CPUがAllwinner H3 512MB 最大1.2GHz、armhfアーキテクチャのQuad coreなので、消費電力や発熱はちょっと心配です。VoCoreよりはRPiに近いんですかね。OSはFriendlyWRT(要するにOpenWRT)、FriendlyCore(Ubuntu Core)、DietPiなどが使用できるようです。あと、NanoPi NEOはNEO2というシリーズがあり、そちらは更にパワーアップ(arm64)しているようですが、先日見たところでは在庫切れでした。興味はありましたが、今回の目的には合わないと考えて、素直に上のNEO Metal Basic Kitにしました。

とにかく安い(VoCoreより安いってどうなってんの)ですが、今の時期にChina Postだとどこかに行ってしまう不安があったので、EMSを選択しました。それだとちょっと高くなってしまうため、予備と遊び用とで、合わせて3台注文。一週間ぐらいで届きました(この時点でVoCoreはまだ届かず…):

(ところで見直してたら気が付いたのですが、ZeroPiの方がよさそうですね。GPIOは全く付いてないですが、Gigabit Ether使えてNEOより安いです。こうも安いとどんぐりの背比べですが。買いなおそうかな…)

もう本当にセットトップボックスとか完成されたかのようなBox linuxマシンなので、あえて蓋も開けていません。実際に電源を入れてみましたが、熱くなるというよりは、ほんのり温かみを感じる、ぐらい(apt upgrade実行中)なので、熱的な心配はRPi 1よりも良さそうです。また、消費電力を測ってみましたが、最大目視で0.45A、アイドル時は0.14Aと、もうこれで良いのでは?という感じです。

Linuxディストリビューションの選択

さて、公式ドキュメントによると、いくつかのファームウェアからインストールしたいOSを選択できるようなんですが… ファイル名にxenialだとかjessieとかで不安しかない。2020年ですよ今。

で、もう少しリサーチしてみると、どうやらArmbianというディストリビューションでDebianかUbuntuライクな実装がコミュニティベースで配布されているようなので、こちらで行くことにしました(一応公式ファームウェアのうち、friendlycoreはユーザー名とパスワードがドキュメント通りではないのでログインも出来ず、jessieはログイン出来たものの、古すぎる上にaptのシグネチャファイル弄ったあとがあるので速攻で破棄…)

Ubuntuに一番慣れているので、Armbian Bionicを選択してwin32diskimagerでSDカードに書き込んで、サクッと起動しました。

(2020/05/20執筆時、ダウンロードしたイメージのバージョンは5.4.20.7でした)

LANを繋いでおいてください。初回起動で30秒かそれ以上待ってから、angryipか何かを使ってIPアドレスを調べます。又は、自動的にDHCPでIPアドレスを取得しているはずで、名前はnanopineoなので、DNS自動レコード更新をやっている環境なら、単にssh nanopineoで繋がると思います。

初回はユーザーroot、パスワード1234で、ログイン時にパスワード更新を求められます。


You are required to change your password immediately (root enforced)
_ _ ____ _ _ _
| \ | | _ \(_) | \ | | ___ ___
| \| | |_) | | | \| |/ _ \/ _ \
| |\ | __/| | | |\ | __/ (_) |
|_| \_|_| |_| |_| \_|\___|\___/

Welcome to Armbian Bionic with Linux 5.4.20-sunxi

System load: 0.00 0.06 0.05 Up time: 7 min
Memory usage: 12 % of 491MB IP: 192.168.***.***
CPU temp: 32°C
Usage of /: 3% of 29G

[ General system configuration (beta): armbian-config ]

Last login: Sat May 23 11:59:19 2020 from 192.168.***.***
Changing password for root.
(current) UNIX password:


ディスクの状態(SANDISK Extreme PRO 32GB V3A1、ファイルシステムの拡張は勝手に行われます)

root@nanopineo:~# df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 181392 0 181392 0% /dev
tmpfs 50336 2928 47408 6% /run
/dev/mmcblk0p1 30391004 778512 29278964 3% /
tmpfs 251680 0 251680 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 251680 0 251680 0% /sys/fs/cgroup
tmpfs 251680 4 251676 1% /tmp
/dev/zram0 49584 884 45116 2% /var/log
tmpfs 50336 0 50336 0% /run/user/0
root@nanopineo:~#

あとは、apt update, apt upgradeで更新。実にスムーズで、ファーストステップまでに何かにハマるところはないと思います。ドキュメント不要でここまで完成度高いと、人に勧めやすい。良くわかってるじゃん(公式配布じゃないけど)。

ArmbianはVoCoreに対応していないのが惜しいな…(MIPSだから仕方がない)

DHCPとDNSの構成はもとのRPi 1からconfig類を持ってきて細部を修正して、isc-dhcp-serverとbind9を入れて、無事に差し替えを完了しました。このconfigは昔自力で作ったんですが、公開しようと思うと色々マスクしなければならず… もしDHCP-DNS自動連携に興味があるなら、この記事を参照すると良いと思います。記事の公開日や想定している環境は古いですが、configの構成のところはほぼ同じです。

UPSの監視もする必要があるのですが、後日やることにします。


おまけ

root@dhcpdns:/home/kouji/test# apt install mono-devel
Reading package lists… Done
Building dependency tree
Reading state information… Done
mono-devel is already the newest version (4.6.2.7+dfsg-1ubuntu1).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
root@dhcpdns:/home/kouji/test# cat Program.cs
using System;

public static class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello mono on NanoPi NEO!");
}
}

root@dhcpdns:/home/kouji/test# mcs Program.cs
root@dhcpdns:/home/kouji/test# mono Program.exe
Hello mono on NanoPi NEO!

root@dhcpdns:/home/kouji/test#

詳しくはビデオで: Windows Formsでマルチプラットフォーム?

Try writing code using both the Azure Sphere Development Kit and C#

This blog post’s 23th entries of “.NET, .NET Core and mono runtimes/frameworks/libraries Advent Calendar 2018 (Japanese)” and “Seeed Users group Advent Calendar 2018 (Japanese)”.

(For Japanese edition, click here / 日本語の記事はこちら)

“Azure Sphere Development Kit” is the evaluation electorical circuit board for “Azure Sphere MCU” produced by Seeed copmpany and the development SDK produced by Microsoft.

“MT3620” is a MCU chip of “Azure sphere specification” designed by MEDIATEK company. In this blog post, it evaluation board is called “MT3620.”

Currently we can design and write a program only “C language” because Azure Sphere SDK limitation, so I tried using for C# language with IL2C.

The IL2C is an open source project I started. It’s a translator for ECMA-335 CIL/MSIL to C language.

(Hosting on GitHub)

IL2C translation process illustrates in this graph. We can write a program with C# language. Then generate the .NET assembly file using the C# compiler (Roslyn).

Next step, IL2C will generate the C language source code both the header “SphereApp.h” and source code “SphereApp.c” files from the assembly file “SphereApp.dll”.

Finally we can build native binary using the target platform C compiler.

In the short word, “Write code using both the Azure Sphere Development Kit and C#” ;)

We’re able to receive advantage for using IL2C, very little footprint less than .NET Core native, ngen and mono with tiny option. I shown demonstration for IL2C on these platforms:

  • Polish notation calculator: On the Win32 native application. (Truly native apps without .NET CLR)
  • Polish notation calculator: The UEFI application. (Truly native apps without any OSes)
  • Polish notation calculator: M5Stack with ten-key block option. (The embedded apps on ESP32)
  • The kernel mode WDM driver, it’s realtime hooking and overwriting for the storage sector fragment.
  • LED arrow feedback with the accelometer sensor on micro:bit. (Both C# and F#)

That’s meaning for we can use the C# language power to any device and any platform. The goal for this blog post, I’ll add Azure Sphere in this list.

If you’ll test this post, we have to receive the device claim at first time use MT3620, I recommend reading for several Azure Sphere SDK documents.

And you can refer the total document for MT3620 in this Seeed studio site.

(By the way, since I don’t have work since next year 2019 ¯\_(ツ)_/¯ If you have a job that will use this solution, please send twitter DM (@kozy_kekyo) so I’m welcome ;)


The strategy: Analyze the LED Blinker sample SDK code

This is a part of the LED Blinker sample code for Azure Sphere development kit. (You can see all code by creating the Blinker sample project at “New solution” menu on Visual Studio 2017.)

/// <summary>
///     Main entry point for this application.
/// </summary>
int main(int argc, char *argv[])
{
    Log_Debug("Blink application starting.\n");
    if (InitPeripheralsAndHandlers() != 0) {
        terminationRequired = true;
    }

    // Use epoll to wait for events and trigger handlers, until an error or SIGTERM happens
    while (!terminationRequired) {
        if (WaitForEventAndCallHandler(epollFd) != 0) {
            terminationRequired = true;
        }
    }

    ClosePeripheralsAndHandlers();
    Log_Debug("Application exiting.\n");
    return 0;
}

I feel there are several topics:

  • We can show the logging message by the “Log_Debug” function.
  • There’re device initialization and finalizing code at the “InitPeripheralsAndHandlers” and the “ClosePeripheralsAndHandlers” functions.
  • The center code has the polling structure.

I analyze at the InitPeripheralsAndHandlers function:

static int InitPeripheralsAndHandlers(void)
{
    struct sigaction action;
    memset(&action, 0, sizeof(struct sigaction));
    action.sa_handler = TerminationHandler;
    sigaction(SIGTERM, &action, NULL);

    epollFd = CreateEpollFd();
    if (epollFd < 0) {
        return -1;
    }

    // ...

These code fragments use the POSIX signals and the epoll API. Azure Sphere contains the Linux kernel (But fully customized, distributed by Microsoft and we can’t do manipulating directly), so our application code can use these APIs.

And, we don’t immediately know where the LED is blinking… The sample contains a lot of code fragments.

I feel this Blinker sample code is too complex and painful for first step!! Because these code are overwork strictly. It contains the discarding resource, multiplex events using epoll API and using the timers. It’s noisy, but we can improve and hiding it if we use C# language abilities!

This is agenda for this post:

  1. Prepare for use the C# language.
  2. Only LED blinking.
  3. Handles button input and completes Blinker like code.
  4. Improves by C# language style.
  5. Comparison binaries between C language and C# language.

Step1: Prepare for use the C# language

This link contains the completed code on the GitHub repository. If you try this sample code, you have to clone from this repo and folloing these steps below:

  1. Enable C# language, C++ (VC++) language and NUnit3 test adapter from extension manager on Visual Studio 2017.
  2. Open the il2c.sln file and build entire solution. If you can’t success building, you can skip next step for the unit tests.
  3. Run all of the unit tests at the Test Explorer. The test execution has to past long time. (Note: It’ll download the MinGW gcc toolchain sets from internet for first time execution.)
  4. Open the samples/AzureSphere/AzureSphere.sln file and build entire solution. This solution contains three projects and it builds automated by predefined project dependencies.

The AzureSphere.sln solution contains these three projects:

  • IL2C.Runtime: Build IL2C runtime code library. It’s VC++ project.
    (It’s referrering to the runtime code file at IL2C repository, so you have to clone entire IL2C repository if you would like to build successful.)
  • MT3620Blink: The Blink project written by C# language. (This blog post focused it.)
  • MT3620App: It’s VC++ project, only deployment usage for MT3620. (Reduced not important code from SDK sample app.)

The C# project file (MT3620Blink.csproj) is written by new simple MSBuild format. We have to choice the target framework moniker values net46 and above or netcoreapp2.0.

And add reference NuGet package named “IL2C.Build.” (version 0.4.22 or above.) It’ll compile by Roslyn and translate by IL2C automatically.

IL2C will store the translated C language source files defaulted into under the directory “$(OutDir)/IL2C/” . We have to change store directory to under the MT3620App project folder, add the “IL2COutputPath” element at this csproj xml node:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net46</TargetFramework>
    <OutputType>Library</OutputType>
    <AssemblyName>MT3620Blink</AssemblyName>

    <!-- ... -->

    <!-- IL2C puts on for translated source files -->
    <IL2COutputPath>$(ProjectDir)../MT3620App/Generated</IL2COutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IL2C.Build" Version="0.4.6" />
  </ItemGroup>
</Project>

The interoperability topics for IL2C

.NET supports interoperability technologies named “P/Invoke.” It’s standard for .NET world and supports by IL2C. But one more option has IL2C named “IL2C/Invoke.”

IL2C/Invoke is easier interoperability for the C language world. For example, if we would like to use “OutputDebugString” the debugger API at the Windows using the P/Invoke, following below:

public static class InteroperabilityDemonstration
{
    // Write message to the debugger console
    [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW", CharSet = CharSet.Unicode)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // We can direct call the native API using standard .NET method rules.
        OutputDebugString(message);
    }
}

The “DllImport” attribute understands by .NET CLR (.NET Core CLR and mono runtime), and the runtime will analyze and call bypassing to the native API inside the “kernel32.dll” dynamic link library.

It’s same as using IL2C/Invoke:

public static class InteroperabilityDemonstration
{
    // Write message to the debugger console
    [NativeMethod("windows.h", SymbolName = "OutputDebugStringW", CharSet = NativeCharSet.Unicode)]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // We can direct call the native API using standard .NET method rules.
        OutputDebugString(message);
    }
}

If we uses IL2C/Invoke, replace the DllImport attribute to the “NativeMethod” attribute and we have to set the C language header file name at the attribute argument.

Because IL2C translates to the C language source code. It has to refer to the API sets via C header files. The NativeMethod attribute translates to the “#include” preprocessor directive.

(And unfortunately, we have to apply with “MethodImpl” attribute because the C# compiler (Roslyn) requires it if method declaration has “extern”.)

IL2C/Invoke likes as P/Invoke for now, but it has advantages, will describe it later.

IL2C/Invoke first example

We can use Azure Sphere SDK with IL2C/Invoke. For first try, will cover the “nanosleep” API:

namespace MT3620Blink
{
  [NativeType("time.h", SymbolName = "struct timespec")]
  internal struct timespec
  {
      public int tv_sec;
      public int tv_nsec;
  }

  public static class Program
  {
      [NativeMethod("time.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern void nanosleep(ref timespec time, ref timespec remains);

      public static int Main()
      {
          var sleepTime = new timespec { tv_sec = 1 };
          var dummy = new timespec();
        
          while (true)
          {
              nanosleep(ref sleepTime, ref dummy);
          }
      }
  }
}

The nanosleep API declarated into the “time.h” header from Azure Sphere SDK. It has the pointer of “timespec” structure type. We have to replace the pointer argument to the ref attribute same as P/Invoke.

And declares timespec value type inside .NET world. We have to apply the “NativeType” attribute to timespec value type. The defaulted name “timespec” overwrites to the symbol named “struct timespec”.

The defaulted name becomes from .NET type name, but SDK’s timespec structure type is NOT typedef’ed, so we have to apply prefix “struct” keyword before name.

It’s IL2C/Invoke advantage. P/Invoke requires binary layout equality both C structure types and the .NET value types. It’s difficult for average developer.

If NativeType attribute applies for the .NET value type, IL2C doesn’t generate the structure fields and places typedef’ed alias to the C structure type by only symbol naming. For example, This is illustrates for IL2C translated C source code (noise stripped):

#include <time.h>

// The alias for "struct timespec"
typedef struct timespec MT3620Blink_timespec;

void MT3620Blink_Program_nanosleep(MT3620Blink_timespec* time, MT3620Blink_timespec* remains)
{
    nanosleep(time, remains);
}

int32_t MT3620Blink_Program_Main(void)
{
    MT3620Blink_timespec sleepTime = { 1 };
    MT3620Blink_timespec dummy;
    // ...
    MT3620Blink_Program_nanosleep(&sleepTime, &dummy);
    // ...
}

The string type (System.String) translates to NULL terminated “wchar_t*”. I’ll support passing “char*” if apply UTF8 flag at the NativeMethod’s CharSet property, and will support StringBuilder class too.

Step2: Only LED blinking

We’re able to finish the Blink code with the GPIO API. It’s illustrated:

namespace MT3620Blink
{
  [NativeType("applibs/gpio.h")]
  internal enum GPIO_OutputMode_Type
  {
      GPIO_OutputMode_PushPull = 0,
      GPIO_OutputMode_OpenDrain = 1,
      GPIO_OutputMode_OpenSource = 2
  }

  [NativeType("applibs/gpio.h")]
  internal enum GPIO_Value_Type
  {
      GPIO_Value_Low = 0,
      GPIO_Value_High = 1
  }

  public static class Program
  {
      [NativeValue("mt3620_rdb.h")]
      private static readonly int MT3620_RDB_LED1_RED;

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_OpenAsOutput(
          int gpioId, GPIO_OutputMode_Type outputMode, GPIO_Value_Type initialValue);

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_SetValue(int gpioFd, GPIO_Value_Type value);

      public static int Main()
      {
          var fd = GPIO_OpenAsOutput(
              MT3620_RDB_LED1_RED,
              GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
              GPIO_Value_Type.GPIO_Value_High);
          var flag = false;

          while (true)
          {
              GPIO_SetValue(fd, flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
              flag = !flag;

              // nanosleep(...);
          }
      }
  }
}

The GPIO output manipulates indirectly from GPIO_SetValue API. It returns the “descriptor” value by 32bit integer. It equals System.Int32 type on the .NET world.

IL2C/Invoke third feature, the “NativeValue” attribute can refer the C language world symbols (macro and anything) directly. But it has limitation because causes C# compiler’s “uninitialized field” warning. Future release for IL2C, maybe it cancel or replace for another methods.

We can blink LEDs by enum type values both GPIO_Value_High and GPIO_Value_Low.

The NativeType attribute is usable too when apply to the enum type. These examples apply to enum types both GPIO_OutputMode_Type and GPIO_Value_Type. Unfortunately, IL2C requires applying to the real numeric value at these enum value field. I’ll improve removing it for future release.

These interoperability code fragment is too ugly for the application layer. We can refactor moving to another class or appling partial class by the C# feature. IL2C doesn’t concern and not problem for using these technics, because it handles only CIL/MSIL (intermediate language.)

Our interoperability code moved to the “Interops” class.

Step3: Handles button input and completes Blinker like code

Next step for the button input, we can handle it with both GPIO_OpenAsInput and GPIO_GetValue API:

internal static class Interops
{
    // ...

    [NativeValue("mt3620_rdb.h")]
    public static readonly int MT3620_RDB_BUTTON_A;

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_OpenAsInput(int gpioId);

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_GetValue(int gpioFd, out GPIO_Value_Type value);
}

If we defined the arguments with “ref” or “out” attributes, IL2C will translate to the C language pointer type. But in this case, GPIO_GetValue’s argument is only output direction. So we can apply “out” attribute. It’s able to use with “out var” C# style syntax sugar.

Completed this step:

// Wait for nsec
private static void sleep(int nsec)
{
    var sleepTime = new timespec { tv_nsec = nsec };
    Interops.nanosleep(ref sleepTime, out var dummy);
}

public static int Main()
{
    var ledFd = Interops.GPIO_OpenAsOutput(
        Interops.MT3620_RDB_LED1_RED,
        GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
        GPIO_Value_Type.GPIO_Value_High);

    var buttonFd = Interops.GPIO_OpenAsInput(
        Interops.MT3620_RDB_BUTTON_A);

    var flag = false;

    // Interval durations (nsec)
    var blinkIntervals = new[] { 125_000_000, 250_000_000, 500_000_000 };
    var blinkIntervalIndex = 0;

    var lastButtonValue = GPIO_Value_Type.GPIO_Value_High;

    while (true)
    {
        Interops.GPIO_SetValue(
            ledFd,
            flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
        flag = !flag;

        // Read button state: we can write with "out var"
        Interops.GPIO_GetValue(buttonFd, out var buttonValue);
        // If changed button state for last reading
        if (buttonValue != lastButtonValue)
        {
            // If current button state is pushing?
            if (buttonValue == GPIO_Value_Type.GPIO_Value_Low)
            {
                // Change interval duration for next value
                blinkIntervalIndex = (blinkIntervalIndex + 1) % blinkIntervals.Length;
            }
        }
        lastButtonValue = buttonValue;

        sleep(blinkIntervals[blinkIntervalIndex]);
    }
}

GPIO_GetValue API is nonblocking-reader. So we have to monitor it has changed.

This code doesn’t have any error test. For example if calls GPIO_OpenAsInput and the API failing, we have to check return value for API. I know we can throw the exception.

Current code applied the ac2018-step1 tag on GitHub.

Step4: Improves by C# language style

It’s bit good. We can use some Visual Studio C# IDE ability at Azure Sphere development. With full power intellisense, refactoring and a lot of extensions.

But maybe you’ll feel not fun, because these code fragments are simply replaced from C language to C# language. Further we have to write additional interoperability declarations.

Azure Sphere SDK’s Blinker sample has one of more topic. Blinker sample uses multiplexing technics by the “epoll” API for timer events both detector the button trigger and LED blinking. (It’s too complex because these reason.)

Our code has a minor issue, can’t detect button trigger at “stop the world” if code call the nanosleep API.

We try to fix these problems with the .NET and C# power!

This graph is the .NET types of we’ll make. We’ll declarate some classes and a interface type and construct these derived topology.

The “Descriptor” class is base class for managing the descriptor value.

The “Application” class receives the instance of IEPollListener interface and will register using epoll API, and aggregates event triggers and dispatching.

The “GpioBlinker” class handles LED blinking by the timer event. The “GpioPoller” class handles LED blinking by the timer event too. Both classes derived from the “Timer” class. It handles timer event and do callback.

Let’s walk through!

First topic, the all APIs handle with the “descriptor value” from opening APIs. We declare the Descriptor class below:

public abstract class Descriptor : IDisposable
{
    public Descriptor(int fd)
    {
        if (fd < 0)
        {
            throw new Exception("Invalid descriptor: " + fd);
        }
        this.Identity = fd;
    }

    public virtual void Dispose()
    {
        if (this.Identity >= 0)
        {
            Interops.close(this.Identity);
            this.Identity = -1;
        }
    }

    protected int Identity { get; private set; }
}

This class marks the base class and implements the “System.IDisposable” interface type. Because the descriptor value is the unmanaged resource, we can use RAII technics by the C# languauge “using” clause.

And throw the exception if descriptor value isn’t valid.

We can implement classes both the GPIO input/output where derive from the Descriptor class:

internal sealed class GpioOutput : Descriptor
{
    public GpioOutput(int gpioId, GPIO_OutputMode_Type type, bool initialValue)
        : base(Interops.GPIO_OpenAsOutput(
            gpioId,
            type,
            initialValue ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low))
    {
    }

    public void SetValue(bool value) =>
        Interops.GPIO_SetValue(
            this.Identity,
            value ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
}

internal sealed class GpioInput : Descriptor
{
    public GpioInput(int gpioId)
        : base(Interops.GPIO_OpenAsInput(gpioId))
    {
    }

    public bool Value
    {
        get
        {
            Interops.GPIO_GetValue(this.Identity, out var value);
            return value == GPIO_Value_Type.GPIO_Value_High;
        }
    }
}

It uses our Interops class. The descriptor value will safe free at the inherited Dispose method.

Next step, the timer function does into a class. The class implements the “IEPollListener” interface, it will use receiving the timer event by the epoll API.

// Receives the timer event by the epoll API
public interface IEPollListener
{
    // The descriptor for managing by epoll API
    int Identity { get; }
    // The callback method for raise the event
    void OnRaised();
}

“Timer” class has to implement this interface, so it can receive event from the epoll API:

internal abstract class Timer : Descriptor, IEPollListener
{
    [NativeValue("time.h")]
    private static readonly int CLOCK_MONOTONIC;
    [NativeValue("time.h")]
    private static readonly int TFD_NONBLOCK;

    protected Timer()
        : base(Interops.timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK))
    {
    }

    public void SetInterval(long nsec)
    {
        var tm = new timespec
        {
            tv_sec = (int)(nsec / 1_000_000_000L),
            tv_nsec = (int)(nsec % 1_000_000_000L)
        };
        var newValue = new itimerspec
        {
            it_value = tm,
            it_interval = tm
        };

        Interops.timerfd_settime(this.Identity, 0, ref newValue, out var dummy);
    }

    // The descriptor for managing by epoll API
    int IEPollListener.Identity => this.Identity;

    // The callback method for raise the event
    void IEPollListener.OnRaised()
    {
        // Consume current timer event.
        Interops.timerfd_read(this.Identity, out var timerData,(UIntPtr)(sizeof(ulong)));
        // Invoke the overrided callback method.
        Raised();
    }

    // The abstract method for timer triggered (Will override)
    protected abstract void Raised();
}

We gonna do derive from it and override the “Raised” method, we can write interval action easier.

The “Application” class handles all events using the epoll APIs and invoke to the target instance implemented IEPollListener interface. The epoll APIs relate for the descriptor value too, so we can inherit from the Descriptor class:

public sealed class Application : Descriptor
{
    public Application()
        : base(Interops.epoll_create1(0))
    {
    }

    // The IEPollListener implemented instance register the epoll API
    public void RegisterDescriptor(IEPollListener target)
    {
        // "Pinned (Fixed)" the instance and get the handle value
        GCHandle handle = GCHandle.Alloc(target, GCHandleType.Pinned);

        // The informations (with the handle) for the epoll API
        var ev = new epoll_event {
            events = Interops.EPOLLIN,
            data = new epoll_data_t { ptr = GCHandle.ToIntPtr(handle) }
        };

        // Register it
        Interops.epoll_ctl(
            this.Identity,
            Interops.EPOLL_CTL_ADD,
            target.Identity,
            ref ev);
    }

    // Unregister the instance
    public void UnregisterDescriptor(IEPollListener target)
    {
        // ...
    }

    public void Run()
    {
        while (true)
        {
            var ev = new epoll_event();
            var numEventsOccurred = Interops.epoll_wait(this.Identity, ref ev, 1, -1);

            if (numEventsOccurred == -1)
            {
                break;
            }
            if (numEventsOccurred == 1)
            {
                // Reconstruct (restore) the handle value uses GCHandle.ToIntPtr method
                GCHandle handle = GCHandle.FromIntPtr(ev.data.ptr);
                // Get the instance reference from the handle value
                var target = (IEPollListener)handle.Target;
                target.OnRaised();
            }
        }
    }
}

It’s same as usage the Windows Forms and the WPF, so I applied this name. It exposes the “RegisterDescriptor” and the “UnregisterDescriptor” methods, these methods register descriptor value sending from he IEPollListener interface to the epoll API. And the “Run” method observes these epoll events.

Maybe you would like to hear what and how works these code fragments. The topic is the “GCHandle” value type. It saves instance references from the garbage collector actions.

This blog post doesn’t explain IL2C’s internal structures, it has the mark-sweep algorithm based garbage collector. If we do “new a class instance”, it’ll place into the heap memory (using the “malloc” function the C runtime.)

The mark-sweep algorithm doesn’t compact memory (not move instance), but automatically free the unreferenced instances (by the “free” function.)

It has two problems at this point:

  • Transfer the instance reference pointer safely.
  • And guards freeing the floating instance by the garbage collector.

(If we use at truly the .NET CLR, .NET Core and mono, we have to save memory compaction. But IL2C doesn’t do it.) These problems can fix using GCHandle value type.

At first, we guard freeing instance, using GCHandle.Alloc method with GCHandle.Pinned argument. The instance isn’t freed even if unreferenced from IL2C.

Next, we can get the real pointer for the instance using the GCHandle.ToIntPtr method. It’s the “System.IntPtr” value type (means “intptr_t” type at the C language.) We can’t any action for this pointer value, it’s opaque but the alias number for this instance.

We must understand the instance absolutely not released if pinned by the GCHandle.Alloc method. It can free again with call the GCHandle.Free method strictly. If we don’t free it, may leak memory. (For more information, see the UnregisterDescriptor method.)

The pointer from the “GCHandle.ToIntPtr”, store into the epoll_data_t structure (nested into the epoll_event structure.) This type applied the NativeType attribute. (The magic shown you again, it’s the union type for C language. IL2C/Invoke easier for use it.):

[NativeType("sys/epoll.h")]
internal struct epoll_data_t
{
    // The epoll_data_t.ptr field is "void*" type on the C language
    public NativePointer ptr;
}

[NativeType("sys/epoll.h", SymbolName = "struct epoll_event")]
internal struct epoll_event
{
    public uint events;
    public epoll_data_t data;
}

The “ptr” field declared by the “NativePointer” type. It real C language type is “void*”, but the C# language can’t use it at the field. We can use NativePointer type in this case, it can transfer from/to the System.IntPtr type.

Then, the related information will store into the “epoll_event” structure if the “epoll_wait” API receives a event. It equals for registered by the “epoll_ctl” API. Therefore we can get the handle value from the “ptr” field using the “GCHandle.FromIntPtr” method.

The instance reference will come from the “GCHandle.Target” property. It type is the System.Object, so can cast to the IEPollListener interface. We made it!

The last step, we have to call the “IEPollListener.OnRaised” method, it delegates to the real class implementation. This is the “Timer.OnRaised” method.

These are the infrastructures. Finally we get the entire Blink samples using it:

public static class Program
{
    private sealed class GpioBlinker : Timer
    {
        private readonly long[] blinkIntervals = new[] { 125_000_000L, 250_000_000L, 500_000_000L };
        private readonly GpioOutput output;
        private bool flag;
        private int blinkIntervalIndex;

        public GpioBlinker(int gpioId)
        {
            output = new GpioOutput(
                gpioId,
                GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
                true);
            this.NextInterval();
        }

        public override void Dispose()
        {
            base.Dispose();
            output.Dispose();
        }

        protected override void Raised()
        {
            output.SetValue(flag);
            flag = !flag;
        }

        public void NextInterval()
        {
            this.SetInterval(blinkIntervals[blinkIntervalIndex]);

            blinkIntervalIndex++;
            blinkIntervalIndex %= 3;
        }
    }

    private sealed class GpioPoller : Timer
    {
        private readonly GpioInput input;
        private readonly GpioBlinker blinker;
        private bool last;

        public GpioPoller(int gpioId, GpioBlinker blinker)
        {
            input = new GpioInput(gpioId);
            last = input.Value;
            this.blinker = blinker;
            this.SetInterval(100_000_000L);
        }

        public override void Dispose()
        {
            base.Dispose();
            input.Dispose();
        }

        protected override void Raised()
        {
            var current = input.Value;
            if (current != last)
            {
                if (!current)
                {
                    blinker.NextInterval();
                }
            }
            last = current;
        }
    }

    public static int Main()
    {
        using (var epoll = new Application())
        {
            using (var ledBlinker = new GpioBlinker(Interops.MT3620_RDB_LED1_RED))
            {
                using (var buttonPoller = new GpioPoller(Interops.MT3620_RDB_BUTTON_A, ledBlinker))
                {
                    epoll.RegisterDescriptor(ledBlinker);
                    epoll.RegisterDescriptor(buttonPoller);

                    epoll.Run();
                }
            }
        }

        return 0;
    }
}

The “GpioBlinker” class blinks the LED, the “GpioPoller” class detects clicking the button and controlling blinking rate by the GpioBlinker. And the “Main” method registers both class instances by the Application class and dispatches the events.

A lot of code fragments are the infrastructure, so we are hard to understand it. But it can the simplist implementation for at the “Program” class. We can design the only concern (We designed LED blinker in this post, you forgot? ;)

The infrastructure can make the library (The .NET assembly.) We can package by the NuGet and can expose it better reusable.

Finished. These code in the ac2018-step2 tag in the repository.

Step5: Comparison binaries between C language and C# language.

Finally, comparison the compiled binary footprint between generated by IL2C and handmaded C source code.

The binary file come from the gcc (not deployment image.) For example location “$(OutDir)/Mt3620App.out” and disable debugger informations using the gcc option “-g0”:

Debug Release
SDK Blinker (C sample code) 16KB 16KB
IL2C Step1 (Non OOP) 111KB 42KB
IL2C Step2 (OOP) 142KB 54KB

The SDK Blinker size same as both Debug and Release. It’s maybe minimum and simplest code, the gcc optimizer can’t do it. The optimizer did hardworks at both IL2C Step1 and Step2. I predict it by IL2C design.

IL2C outputs are simpler (but easier detects optimizing topics at the C compiler) and predicts eliminating the unused symbols/references by the gcc options (-fdata-sections -ffunction-sections -Wl,–gc-sections).

The IL2C’s runtime code is small now, but gonna do larger if merge the corefx library. Even so, this premise is important because we want binaries to contain only the minimum necessary code.

Another design, IL2C uses the standard C runtime library as much as we can. Because larger the footprint if IL2C implements self-designed functions.

For example, the “System.String” class has to the string manipulation methods. IL2C’s runtime delegates to the C library called “wcslen”, “wcscat” and etc. And the garbage collector only uses strategy for mark-sweep with the “malloc” and “free” standard C heap backends. (It means don’t use memory compaction technics.)

However, it is still somewhat large compared to the size of SDK Blinker. It is still feeling that effort is missing…

I still have various ideas to make footprint smaller, so I will try it in a future version.

Conclusion

I tried to write modernized C# style code using IL2C on the Azure Sphere MCU based MT3620 evaluation board. I already did feedback known problems and suggestions. The “Dogfooding” makes better and improves the projects.

The likely projects come the world. It uses the LLVM based. It makes C code from Roslyn abstract syntax tree directly. It’s bootable independent OS with C#, it surprised me.

The IL2C different to these projects. It’ll have and focus the portability and footprint. It attracts the .NET powers to the everywhere.

Azure Sphere Development Kitで C# を使ってコードを書いてみる

この記事は、.NET, .NET Core, monoのランタイム・フレームワーク・ライブラリ Advent Calendar 2018と、Seeed UG Advent Calendar 2018の23日目のダブルエントリーです(遅刻)。

(For English edition, click here)

Azure Sphere Development Kitとは、 Seeedさんが出しているAzure Sphere評価ボード と、開発用SDKのことです。リンクはこのSeeed公式のページからでも買えますが、国内にも代理店がいくつかあるので、今ならそちらから買うほうが入手性も良いでしょう。

MT3620とは、 MEDIATEK社が開発した、Azure Sphereの仕様に対応したMCU(CPUチップ) です。そして、上記のSeeedさんが販売しているボードはMT3620を載せた、評価用の開発ボードです。なので、ちょっと語弊あるのですが、この評価ボードの事を、以降MT3620と呼びます。

で、何でこれをAdvent Calendarのネタにしたかと言うと、現状のAzure Sphere SDKは、「C言語」でのみ開発が可能というもので… .NET界隈の読者はもう言わなくてもわかりますよね ;)

Seeed UGから来られた方は、タイトルにはC言語じゃなくてC#って書いてあるんだけど? と、若干意味不明に思うかも知れないので、以下に背景を書いておきます。

私のblogをちょっと遡ると 「IL2C」 に関する記事が沢山出てきますが、これは私が始めた「.NETのバイナリをC言語のソースコードに変換する」オープンソースのプロジェクトの事です。まだ完成していなくて、開発途上です。

開発を始めたのは、丁度1年半ぐらい前からです。1年前には、 .NET Conf 2017 とか、いくつかの勉強会やグロサミやらで細部とか進捗とかを発表していました。今年はつい先日開催した dotNET600 2018 でも発表しています。あと、YouTubeに大量にIL2Cを解説したビデオを公開しています。

資料などはそれぞれの勉強会サイトやGitHubをあたってもらえると良いと思いますが、ざっくり言うと、この図のとおりです。

C#でプログラムを書いて(もちろん、MT3620向けに)、それを普通にC#のプロジェクトとしてビルドします。そうすると、”SphereApp.dll”のような.NETのバイナリが出来ます。これをIL2Cに掛けると、C言語のソースコードに変換されたファイルが生成されます。これらのソースコードファイルを、ターゲットデバイスのC言語コンパイラ(この記事ではAzure Sphere SDKが提供するgcc)に掛けて、デバイス用のネイティブバイナリを生成します。

つまり簡単に言えば、この記事のタイトル通り、「Azure Sphere Development Kitで C# を使ってコードを書く」と言うことになるわけです。

IL2Cの最大の特徴であり目標は、例えば.NET Coreのネイティブビルドやngen、monoの極小ビルドと比較しても、到底不可能なぐらいにフットプリントを小さく出来て、内部構造がシンプルで、ネイティブコードとの連携をコントローラブルにする、というものです。これまでにデモンストレーションで作ってみたものとしては:

  • 簡易電卓: Win32ネイティブコンソールアプリケーション(つまり.NET CLRを使わず、Win32ネイティブで動く)
  • 簡易電卓: UEFIアプリケーション(つまりOSなしで動く電卓をC#で書いたって事です)
  • 簡易電卓: M5Stack テンキーブロック(ESP32上で組み込みソフトウェアとして)
  • ディスクのセクタデータをリアルタイムに書き換える、カーネルモードWDM(つまりWindowsカーネルモードで動くコードをC#で書いたって事です)
  • micro:bitで、加速度センサーを使用してLEDに方向をフィードバックするプログラム(これはC#だけではなく、F#でもデモしました)

という感じで、極小リソースのデバイスでも.NET ILを変換したネイティブコードを、インタプリタではなくAOTコンパイルしたコードで動かせるという実証や、UEFIやWDMなどの極めて特殊な環境でも動作するという実証実験をしています。今回はここに、Azure Sphereを加えるという目標です。

ということで、

  • MT3620のセットアップの流れ
  • サンプルコードの構造の確認とC#での実現方法

の大きく二章立て構成で解説します。

(ところで、来年から仕事無いので、これをやらせてくれる仕事ありましたら、凄く嬉しいのでDMください)


MT3620のセットアップの流れ

MT3620は割と早い時期に入手していたのですが、(某人がこれの環境構築に四苦八苦してたのを横目で見てたこともあり…)色々忙しくて放置してました。なので、今の時期にデバイスクレーム(MT3620の登録作業)からやる話は、同様にこれから始める人向けにも良いんじゃないかと思ってます。

@linyixian さんの、Azure Sphere 開発ボードでLチカをやってみる が非常に参考になりました。ほぼこの通りで出来ましたが、私固有の問題が少しあったので、ログ含めてセットアップの記録を残しておきます。

なお、使用したAzure Sphere SDKのバージョンは “18.11.3.23845” でした。SDKもそこそこバージョンアップされているので、あまりバージョンがかけ離れると、ここに書いてあることも無意味になるかも知れません。

さて、Azure AD面倒くさいんですが、組織アカウントを作っておくところまでは、記事通りやっておきます。私の場合、個人的にいくつかのサブスクリプションと組織が紐付いている状態で実験したりしていた関係で、シンプルに無料チャージから始めた方とは事情が違ってしまっている可能性があります。Azure ADはいつ作ったか作ってないのかわからないものが一つだけあったので、この中に組織アカウントを作りました(管理的に問題のある選択だったかも知れない… 消さないようにしなければ)。

Azure ADに組織アカウントを作ったら、Azure Sphereテナントを作ります。テナントはAzure Sphere SDKのコマンドプロンプトで作業します。つまり、ここからの操作はAzureのダッシュボードは不要です。が…

C:\>azsphere tenant create -n ***********
error: An unexpected problem occurred. Please try again; if the issue persists, please refer to aka.ms/azurespheresupport for troubleshooting suggestions and support.
error: Command failed in 00:00:39.0889958.

何故か失敗(以下、ログはIDとか伏せ字です)…

しかも意味不明なエラーメッセージなので、こりゃ早速サポート案件か… と思ったのですが、@linyixian さんのLチカ記事読み直すと、この時点でMT3620をUSB接続していたようなので(テナント生成にデバイス関係あるの?という疑問は残るものの)MT3620を接続して試したら、うまく行きました:

C:\>azsphere tenant create -n ***********
warn: Your device's Azure Sphere OS version (TP4.2.1) is deprecated. Recover your device using 'azsphere device recover' and try again. See aka.ms/AzureSphereUpgradeGuidance for further advice and support.
Created a new Azure Sphere tenant:
 --> Tenant Name: ***********
 --> Tenant ID:   ***********
Selected Azure Sphere tenant '***********' as the default.
You may now wish to claim the attached device into this tenant using 'azsphere device claim'.
Command completed successfully in 00:00:23.2329116.

(そう言えばMatsuoka氏も最初の頃そんな事を言っていた事を思い出した)

ただ、上記のように、私のMT3620はファームウェアが古すぎだと言われた(まあ、放ったらかしにしてたしね)ので、早速suggestされたコマンドで更新をかけました。

C:\>azsphere device recover
Starting device recovery. Please note that this may take up to 10 minutes.
Board found. Sending recovery bootloader.
Erasing flash.
Sending images.
Sending image 1 of 16.
Sending image 2 of 16.
Sending image 3 of 16.
Sending image 4 of 16.
Sending image 5 of 16.
Sending image 6 of 16.
Sending image 7 of 16.
Sending image 8 of 16.
Sending image 9 of 16.
Sending image 10 of 16.
Sending image 11 of 16.
Sending image 12 of 16.
Sending image 13 of 16.
Sending image 14 of 16.
Sending image 15 of 16.
Sending image 16 of 16.
Finished writing images; rebooting board.
Device ID: ***********
Device recovered successfully.
Command completed successfully in 00:02:41.2721537.

難なく、更新されたようです。では、いよいよ後戻りできない、MT3620のデバイスクレームを実行します:

C:\>azsphere device claim
Claiming device.
Successfully claimed device ID '***********' into tenant '***********' with ID '***********'.
Command completed successfully in 00:00:04.6818769.

これで、このMT3620はこのAzure Sphereテナントに登録されてしまったので、もう誰にも譲れなくなりました… (Azure Sphere MCU対応デバイスは、デバイスクレームを一度しか実行できません。詳しくはググってみてください)

さて、気を取り直して、WiFi出来るようにしておきます。

C:\>azsphere device wifi add --ssid *********** --key ***********
Add network succeeded:
ID                  : 0
SSID                : ***********
Configuration state : enabled
Connection state    : unknown
Security state      : psk

Command completed successfully in 00:00:01.7714741.

WiFi接続ができているかどうかを確認

C:\>azsphere device wifi show-status
SSID                : ***********
Configuration state : enabled
Connection state    : connected
Security state      : psk
Frequency           : 5500
Mode                : station
Key management      : WPA2-PSK
WPA State           : COMPLETED
IP Address          : ***********
MAC Address         : ***********

Command completed successfully in 00:00:01.0424492.

いい感じです。一度動き出せば、管理は非常に簡単です。WiFi構成はいつでも上記コマンドで変更できます。

最後に、MT3620を「開発モード」にします。

MT3620自体はDevKitなので、文字通り「開発」に使わないのなら何に使うんだ? ということで、この操作は意味不明に思われるかも知れません(特にArduinoやESP32のようなプラットフォームを触っていると)。冒頭で触れたように、元々のAzure Sphere MCUは、様々な組み込み機器のCPUとして使われることを想定しており、真にIoTデバイスとして使う場合は、逆に開発モードと言う危なげな状態にならないほうが良いわけです。

この辺は、最近のスマートフォンでも同じように制限を掛けていると思うので、それと同じように考えればわかりやすいと思います。では、開発モードに変更します:

C:\>azsphere device prep-debug
Getting device capability configuration for application development.
Downloading device capability configuration for device ID '***********'.
Successfully downloaded device capability configuration.
Successfully wrote device capability configuration file 'C:\Users\k\AppData\Local\Temp\tmp6CD5.tmp'.
Setting device group ID '***********' for device with ID '***********'.
Successfully disabled over-the-air updates.
Enabling application development capability on attached device.
Applying device capability configuration to device.
Successfully applied device capability configuration to device.
The device is rebooting.
Installing debugging server to device.
Deploying 'C:\Program Files (x86)\Microsoft Azure Sphere SDK\DebugTools\gdbserver.imagepackage' to the attached device.
Image package 'C:\Program Files (x86)\Microsoft Azure Sphere SDK\DebugTools\gdbserver.imagepackage' has been deployed to the attached device.
Application development capability enabled.
Successfully set up device '***********' for application development, and disabled over-the-air updates.
Command completed successfully in 00:00:32.3129153.

これで開発モードに切り替えできました。

後は、@linyixian さんのLチカの記事通り、Blinkサンプルを実行してLチカ出来る、言い換えればDevKitの用を成しているかどうかをを確認しておきます。

これで、MT3620が使える状態になったかどうかの確認は完了です。

DevKitの詳細については、

を参照すると良いでしょう。


サンプルコードの構造の確認とC#での実現方法

で、ここからが本題なのですが、とりあえず先程のBlinkのサンプル(このサンプルはあくまでC言語)を眺めてみると、以下のような定義が見つかります:

/// <summary>
///     Main entry point for this application.
/// </summary>
int main(int argc, char *argv[])
{
    Log_Debug("Blink application starting.\n");
    if (InitPeripheralsAndHandlers() != 0) {
        terminationRequired = true;
    }

    // Use epoll to wait for events and trigger handlers, until an error or SIGTERM happens
    while (!terminationRequired) {
        if (WaitForEventAndCallHandler(epollFd) != 0) {
            terminationRequired = true;
        }
    }

    ClosePeripheralsAndHandlers();
    Log_Debug("Application exiting.\n");
    return 0;
}

なんだか、至って普通の、なんのヒネリもないmain関数です。あと、いくつか気がつくことがあります:

  • Log_Debug関数でログが出せそう。
  • InitPeripheralsAndHandlersとClosePeripheralsAndHandlersでデバイスの初期化処理と終了処理らしきことをしている。これはSDKの関数ではなく、このサンプル内にコードがあるので、すぐ後で見てみます。
  • ど真ん中にポーリングのようなコードがある。コメントに”SIGTERM”とかコメントがある。

ぐらいですかね。InitPeripheralsAndHandlersを見てみると:

static int InitPeripheralsAndHandlers(void)
{
    struct sigaction action;
    memset(&action, 0, sizeof(struct sigaction));
    action.sa_handler = TerminationHandler;
    sigaction(SIGTERM, &action, NULL);

    epollFd = CreateEpollFd();
    if (epollFd < 0) {
        return -1;
    }

    // ...

えぇ… POSIX signal使えるんだ… ちょっと予想外だった。いや、やや不透明ですが、内部ではLinuxカーネルが動いているので、当然といえば当然かも知れません。同様に、main関数のポーリングのようなコードは、epollと関係あるのかも知れません。

また、C言語なのである程度は仕方がないのですが、質素なLチカのサンプルコードなのにグローバル変数がてんこ盛りかつ複雑なので、これで何かしようというやる気が初っ端からへし折られていく感じがします… (一応擁護しておくなら、頑張ってきれいに書こうとしている匂いはします)

では、そろそろ今回やろうとしていることの本題に入りましょうか。

  1. 本格的に何か出来るようにしようとすると、Advent Calendarでは収集つかなくなりそうなので、Blink同様のものを目標にします。
  2. 真のLチカ(つまり、LEDの点滅のみ)をやってみます。
  3. ボタン入力に対応させてみます。
  4. C#らしいコードにします。
  5. 生成されたコードを比較します。

ビルド環境の準備

完成したサンプルコードはここにあります。

上記サンプルコードを動かす場合は、リポジトリ全体をcloneした後、以下のように準備しておきます。

  1. Visual Studio 2017にC#, C++(VC++), NUnit3 test adapter(拡張機能から)が入っている必要があります。普通はVC++入れないと思いますが、Azure Sphere SDKを入れてあるなら入っているでしょう。
  2. ルートにあるil2c.slnを開いてビルドします。どうしてもビルドが通らない場合は、3のテストは諦めることも出来ます(.NET Framework 4.0以降・.NET Core 1.0以降・.NET Standard 1.0以降の全てのバージョンに依存しているため、環境によってはビルドできないかも知れません。Visual Studio Installerを起動して、とにかく全てのコンポーネントをインストールしてみるのはありかも知れません。私は常に全部入りです)。
  3. Test Explorerからテストを実行して全部パスすることを確認しておきます(結構時間がかかります。CPUコア数多いと速いです。i7-4790Kで5分ぐらい。あと、1GBぐらいのディスクスペースが必要で、初回実行時に一度だけMinGWのgcc toolchainをダウンロードするため、遅い回線だと辛いかもしれません)。masterブランチには安定的に動作するコードが入っているはずですが、テストを実行しておけば、少なくとも私が見た結果と同じものが得られるはずです。
  4. samples/AzureSphere/AzureSphere.slnを開いてビルドしてください。このソリューリョンには3つのプロジェクトが含まれていますが、依存関係が正しく設定されているはずなので、ビルドすれば全てが正しく構築されるはずです。

AzureSphere.slnソリューションに含まれるプロジェクトは、以下のとおりです:

  • IL2C.Runtime: IL2Cのランタイムコードをライブラリ化するVC++プロジェクト
    (このプロジェクトはIL2Cリポジトリに含まれるランタイムのソースファイルを参照しているので、このプロジェクトをビルドするためにリポジトリ全体が必要です)
  • MT3620Blink: C#で書かれたBlinkのプロジェクト(以降で説明するC#で書くコード)
  • MT3620App: MT3620向けのデプロイ用VC++プロジェクト(SDKのサンプルコードから不要なコードを省いたものです)

以降の解説は、上記プロジェクトのうちのC# Blinkのプロジェクトを一から書く場合の解説です。

このC#プロジェクトは、いわゆるMSBuildの新形式(.NET Coreや.NET Standardで使われている、新しいシンプルなcsprojファイル)ライブラリプロジェクトを使って、net46以上かnetcoreapp2.0以降をターゲットにします。そして、NuGetパッケージのIL2C.Build 0.4.22以降(この記事を書いた時点の最新)を参照してください。

これでビルド時に裏でIL2Cが自動的に実行され、C言語のソースコードが出力されるようになります(IL2Cのバイナリを明示的にインストールしたりする必要はありません)。

(説明が前後しますが、C#プロジェクト自体は、IL2C.Buildパッケージを参照するだけで作ることが出来ます。今回、IL2Cのリポジトリ全体をcloneするのは、IL2C.Runtimeのビルドのためです。この辺りはスマートではない事は認知しているので、将来的にはもっと簡単に出来るようにしたいと思っています)

C言語のソースコードは、デフォルトでは$(OutDir)/IL2C/の下に出力されます。今回は、別のVC++プロジェクト配下のGeneratedフォルダに出力したいため、csprojのPropertyGroupに以下のように1行加えておきます:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net46</TargetFramework>
    <OutputType>Library</OutputType>
    <AssemblyName>MT3620Blink</AssemblyName>

    <!-- ... -->

    <!-- C言語ソースコードの出力先フォルダを指定 -->
    <IL2COutputPath>$(ProjectDir)../MT3620App/Generated</IL2COutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IL2C.Build" Version="0.4.6" />
  </ItemGroup>
</Project>

なお、旧形式のMSBuild、つまり.NET Frameworkで使われているcsprojのライブラリプロジェクトでも出来ますが、IL2C.Interopパッケージが正しく参照されないため、手動でこのパッケージも追加する必要があります。

また、net46より古いか、netstandard辺りのプラットフォームをターゲットとすることも出来ますが、現状のIL2C.Buildではビルドできないので、IL2C.exeを自力で呼び出す必要があります(PostBuildEventに書きます。引数の種類は限られているのでそれほど難しくはありませんが、ここでは省略します)。

IL2CのInteroperability(相互運用機能)

.NETの世界で Interoperability(相互運用)と言うと、.NETのコードから、ネイティブのコード(例えばWin32やLinuxのAPI)を呼び出したりする事を指します。.NETのコードは、全てが.NETだけで実現されているわけではなく、例えばファイルの入出力やネットワークアクセスなどは、OSのAPIを呼び出して実現する必要があります。

普通のプログラムは、これらの「外部リソース」にアクセスします。したがって、如何に簡単にプログラムの処理系からAPIを呼び出せるかが鍵となります。.NETの場合、標準の相互運用機能に P/Invoke と呼ばれる機能があり、これを使って相互運用を実現します。

例えば、WindowsのWin32 APIにはデバッグAPIがあって、デバッグ用の文字列を出力することが出来ます(デバッガのログに表示されます)。これを.NET上から呼び出すには、以下のようなコードを書きます:

public static class InteroperabilityDemonstration
{
    // Win32 APIのデバッガAPIへの呼び出しを定義する
    [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW", CharSet = CharSet.Unicode)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // デバッガに文字列を出力する
        OutputDebugString(message);
    }
}

詳細は省きますが、DllImportという属性を適用したメソッドは、対応するWin32 APIと紐付けられ、まるで.NETにそのメソッドが存在するかのように呼び出すことが出来るようになります。

例としてWin32 APIを挙げましたが、.NET Coreやmonoで想定されるマルチプラットフォーム(つまりLinuxやMac)でも、全く同じ方法でネイティブのAPIを呼び出すことが出来ます。

IL2Cでは、P/Invokeを踏襲した相互運用機能を実現するのと同時に、独自の相互運用機能も用意しています(注:現状、どちらの実装もまだ完全ではありません)。この、独自の相互運用機能(便宜上、IL2C/Invokeと呼びます)で同じ事を実現する場合は、以下のように書きます:

public static class InteroperabilityDemonstration
{
    // Win32 APIのデバッガAPIへの呼び出しを定義する
    [NativeMethod("windows.h", SymbolName = "OutputDebugStringW", CharSet = NativeCharSet.Unicode)]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // デバッガに文字列を出力する
        OutputDebugString(message);
    }
}

何故、IL2C/Invokeを用意したのかは、このコードを注意深く見ると気がつくかも知れません。P/InvokeのDllImport属性は、対象のAPIが「ダイナミックリンクライブラリ」に含まれている事を前提としています。kernel32.dllを参照することで、プログラムの実行時に指定されたライブラリを「動的」に読み込んで、APIの呼び出しを可能にします。

しかし、IL2CはC言語のソースを出力し、それらがすべて事前にコンパイルされる必要があります。参照すべきAPIもコンパイル時に解決される必要があるため、ライブラリ本体ではなく、C言語のヘッダファイルへの参照が必要なのです。当初はDllImport属性を流用していたのですが、このような理由から独自の属性を定義して区別するようにしました。また、区別したことで、後で述べるような利点も生まれています。

唯一、MethodImpl属性が邪魔ですね。これはC#コンパイラが要求していて省略できないため、どうしても一緒に書いておく必要があります。今のところ回避方法はありません。

Azure Sphere SDKのAPIを参照する

このIL2C/Invokeを使って、Azure Sphere SDKのAPIを.NETから使えるようにしてみます。まずはnanosleep APIをカバーしてみましょう。

namespace MT3620Blink
{
  [NativeType("time.h", SymbolName = "struct timespec")]
  internal struct timespec
  {
      public int tv_sec;
      public int tv_nsec;
  }

  public static class Program
  {
      [NativeMethod("time.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern void nanosleep(ref timespec time, ref timespec remains);

      public static int Main()
      {
          var sleepTime = new timespec { tv_sec = 1 };
          var dummy = new timespec();
        
          while (true)
          {
              nanosleep(ref sleepTime, ref dummy);
          }
      }
  }
}

nanosleep APIは、Azure Sphere SDKのtime.hに定義されています。そして、引数にtimespec構造体へのポインタを取ります。P/Invokeで使われるのと同じ技法で、ポインタはrefで参照するようにします。

また、.NETから構造体を参照できるように、timespec構造体を宣言します。ここに、NativeType属性を適用して、この構造体がどのヘッダファイルに定義されているのかを宣言し、合わせて実際の構造体の宣言名(ここでは “struct timespec”)をSymbolNameプロパティで適用しておきます。

宣言名を省略すれば、.NET構造体の名前がそのまま使用されます。しかし、SDKのtimespec構造体はtypedefで別名宣言されていないため、”struct”の接頭語を付けておく必要があります(Azure Sphere SDKはC++をサポートしていないので、この場合は接頭語を省略できません)。

先程、IL2C/Invokeには利点があるという話をしましたが、このNativeType属性が正にそれです。P/Invokeの場合、定義する構造体の型は、バイナリレイアウトを含めて完全な状態で再定義しなければならず、比較的難易度の高い作業でした。

NativeType属性が適用された構造体は、基本的にシンボル名と型が合っていれば、問題なくネイティブコードと結合できます。何故なら、NET構造体の定義はIL2Cによって無視され、C言語の構造体を(定義が同一であるという前提で)そのまま使うからです。

以下に、変換結果を概念的にしたものを示します:

#include <time.h>

// struct timespec構造体の別名を定義
typedef struct timespec MT3620Blink_timespec;

void MT3620Blink_Program_nanosleep(MT3620Blink_timespec* time, MT3620Blink_timespec* remains)
{
    nanosleep(time, remains);
}

int32_t MT3620Blink_Program_Main(void)
{
    MT3620Blink_timespec sleepTime = { 1 };
    MT3620Blink_timespec dummy;
    // ...
    MT3620Blink_Program_nanosleep(&sleepTime, &dummy);
    // ...
}

C言語のソースコード中に、timespec構造体の詳細な定義が無く、typedefを使って本物のtimespec構造体にバイパスしているだけです。

なお、文字列(.NETのSystem.String型)は、NULLで終端されている wchar_t* が渡されるようになっています。NativeMethod属性のCharSetにUTF8を指定することで char* を渡すことも出来るようにしたり、P/Invokeと同じように、StringBuilderクラスを使ったやり取りも出来るようにする予定です。

GPIO APIをカバーする

GPIO APIをカバーして、Lチカを完成させます。以下に、GPIOにアクセスする部分だけを抜き出したC#のコードを載せます:

namespace MT3620Blink
{
  [NativeType("applibs/gpio.h")]
  internal enum GPIO_OutputMode_Type
  {
      GPIO_OutputMode_PushPull = 0,
      GPIO_OutputMode_OpenDrain = 1,
      GPIO_OutputMode_OpenSource = 2
  }

  [NativeType("applibs/gpio.h")]
  internal enum GPIO_Value_Type
  {
      GPIO_Value_Low = 0,
      GPIO_Value_High = 1
  }

  public static class Program
  {
      [NativeValue("mt3620_rdb.h")]
      private static readonly int MT3620_RDB_LED1_RED;

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_OpenAsOutput(
          int gpioId, GPIO_OutputMode_Type outputMode, GPIO_Value_Type initialValue);

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_SetValue(int gpioFd, GPIO_Value_Type value);

      public static int Main()
      {
          var fd = GPIO_OpenAsOutput(
              MT3620_RDB_LED1_RED,
              GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
              GPIO_Value_Type.GPIO_Value_High);
          var flag = false;

          while (true)
          {
              GPIO_SetValue(fd, flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
              flag = !flag;

              // nanosleep(...);
          }
      }
  }
}

MT3620のGPIOは、GPIO_OpenAsOutput APIで出力用にデバイスをオープンして、その時得られたディスクリプタをGPIO_SetValue APIに渡すことで操作します。入力の場合はまた別のAPIを使いますが、まずはLチカを実現しましょう。

ディスクリプタは32ビット整数(C言語でint)なので、.NETでもint(System.Int32)でOKですね。戻り値で得られるので、これをローカル変数に保存しておきます。

NativeValue属性もIL2C/Invokeで使用する属性で、定数シンボル(C言語マクロなど)を参照出来るようにするために使用します。但し、staticフィールドの初期値を割り当てないため、コンパイル時に警告が発生し、具合がよくありません。将来のバージョンではやり方を変えるかも知れません。

ループ内で、現在のフラグに応じて、GPIO_Value_HighとGPIO_Value_Lowを切り替え、LEDの点滅を実現します。

ここでもNativeType属性を便利に使うことが出来ます。列挙型GPIO_OutputMode_TypeとGPIO_Value_Typeを、C言語の列挙型に対応付けています。残念ながら、今はまだ、列挙型の値(数値)をC言語の定義と一致させる必要があります(将来的には省けるようにしようと考えています)。

このコードを俯瞰すると、C#でP/Invokeを使ったことがある人なら「この醜い相互運用の定義は、別のクラスに移動しておこう。そうすれば、Mainメソッドがスッキリする」と想像出来ると思います。

実際、別のクラスに定義を移動しても問題ありませんし、フットプリントが気になるならpartial classを使えば良いでしょう。IL2CはC#のソースファイルではなく、コンパイルされたアセンブリファイル(MT3620Blink.dll)に対して処理を行うからです。

(というように各自で工夫してくれると良いなーと思ってたのですが、Matsuoka氏に見せたら初見で同じことをつぶやいたので、まあ作戦は成功したと言えるでしょう :)

サンプルコードでは、Interopsというクラスを作り、そこに移動しました。

横道にそれましたが、ボタン入力をGPIO APIで取得するには、GPIO_OpenAsInput APIでオープンしてから、GPIO_GetValue APIを使います。これらは以下のように定義できます:

internal static class Interops
{
    // ...

    [NativeValue("mt3620_rdb.h")]
    public static readonly int MT3620_RDB_BUTTON_A;

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_OpenAsInput(int gpioId);

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_GetValue(int gpioFd, out GPIO_Value_Type value);
}

IL2Cでは、ref引数でもout引数でもポインタ渡しになります。しかし、GPIO_GetValueは(ポインタ経由で)値を読み取ることは無いのでoutにしておきます。すると、メソッドを使う側ではout varを使って簡単に書けるようになります。nanosleepも直しておきます。全体的なコードは以下の通りです:

// 指定されたnsecだけ待機する
private static void sleep(int nsec)
{
    var sleepTime = new timespec { tv_nsec = nsec };
    Interops.nanosleep(ref sleepTime, out var dummy);
}

public static int Main()
{
    var ledFd = Interops.GPIO_OpenAsOutput(
        Interops.MT3620_RDB_LED1_RED,
        GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
        GPIO_Value_Type.GPIO_Value_High);

    var buttonFd = Interops.GPIO_OpenAsInput(
        Interops.MT3620_RDB_BUTTON_A);

    var flag = false;

    // 待機時間(nsec)
    var blinkIntervals = new[] { 125_000_000, 250_000_000, 500_000_000 };
    var blinkIntervalIndex = 0;

    var lastButtonValue = GPIO_Value_Type.GPIO_Value_High;

    while (true)
    {
        Interops.GPIO_SetValue(
            ledFd,
            flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
        flag = !flag;

        // ボタンの状態を読み取る: out varで簡単に書ける
        Interops.GPIO_GetValue(buttonFd, out var buttonValue);
        // 直前のボタンの状態から変化していれば
        if (buttonValue != lastButtonValue)
        {
            // ボタンが押されていれば(ボタンの信号は論理が逆なので注意)
            if (buttonValue == GPIO_Value_Type.GPIO_Value_Low)
            {
                // 待機時間を変更
                blinkIntervalIndex = (blinkIntervalIndex + 1) % blinkIntervals.Length;
            }
        }
        lastButtonValue = buttonValue;

        sleep(blinkIntervals[blinkIntervalIndex]);
    }
}

ボタンの状態は瞬時に読み取られるので、連続的に待機時間が変わってしまわないように、ボタン状態の変化を見るようにしています。

Azure Sphere SDKのBlinkerサンプルは、ディスクリプタの取得に失敗した場合(-1が返されるなど)の処理もきちんと実装していますが、このコードでは省略しています。もしやるならC#らしく、例外を定義してスローするという方法が考えられます。

なお、ここまでのコードは ac2018-step1のタグ で保存してあります。

応用: epollを使って疑似イベント駆動にする

最初に取り組むLチカ+αの課題はこれで十分だと思いますが、C#を使っているのにC言語でプログラムを書いているのとあまり変わりがなく、IL2C/Invokeのための記述が増えるだけで面白みがありませんね。

また、Azure Sphere SDKのBlinkerサンプルは、実装がもう一捻りしてあります(それであんなに複雑なことになっているのですが…)。それは、ボタンクリックのトリガーと点滅を、それぞれタイマーを割り当てて、タイマー経過をepollを使ってマルチプレクスして、擬似的なイベント駆動として処理しているのです。実際、前述のコードには些末ですが問題があります。それは、sleepを呼び出している間は、ボタンの入力が受け付けられないことです。

最後にこの処理を、.NETとC#のパワーでキレイに対処して見ましょう。

この図は、これからC#で作る型の関係を示したものです。いくつかのクラスとインターフェイスを定義して、このような継承関係を構築します。

Descriptorクラスは、このすぐ後で説明する、APIのディスクリプタを管理する基底クラスです。Applicationクラスが、IEPollListenerインターフェイスを実装するインスタンスを受け取ってepollに登録し、発生するイベントの集約と配信を行います。

GpioBlinkerとGpioPollerクラスが、LEDの点滅とボタンのトリガーを監視します。両方共にTimerクラスを継承して、インターバルタイマーの経過を使って処理を行います。

それでは、順を追って説明します。

まず、すべてのAPIへのアクセスは、共通の「ディスクリプタ」で行われている、という事実に着目し、ディスクリプタを管理するクラスを用意します:

public abstract class Descriptor : IDisposable
{
    public Descriptor(int fd)
    {
        if (fd < 0)
        {
            throw new Exception("Invalid descriptor: " + fd);
        }
        this.Identity = fd;
    }

    public virtual void Dispose()
    {
        if (this.Identity >= 0)
        {
            Interops.close(this.Identity);
            this.Identity = -1;
        }
    }

    protected int Identity { get; private set; }
}

このクラスのポイントは、基底クラスとして継承可能にしておき、IDisposableインターフェイスを実装してディスクリプタを破棄可能にします。コンストラクタで渡されたディスクリプタが負数の場合は、例外をスローしておきます。

このクラスを継承して、GPIOのIn,Outを実装してみます:

internal sealed class GpioOutput : Descriptor
{
    public GpioOutput(int gpioId, GPIO_OutputMode_Type type, bool initialValue)
        : base(Interops.GPIO_OpenAsOutput(
            gpioId,
            type,
            initialValue ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low))
    {
    }

    public void SetValue(bool value) =>
        Interops.GPIO_SetValue(
            this.Identity,
            value ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
}

internal sealed class GpioInput : Descriptor
{
    public GpioInput(int gpioId)
        : base(Interops.GPIO_OpenAsInput(gpioId))
    {
    }

    public bool Value
    {
        get
        {
            Interops.GPIO_GetValue(this.Identity, out var value);
            return value == GPIO_Value_Type.GPIO_Value_High;
        }
    }
}

各APIへのアクセスは、前章で作ったInteropsクラスの定義をそのまま流用しています。また、ディスクリプタはDisposeメソッドで解放されるため、破棄についてはここでは何もしていません。特に説明不要で理解できると思います。

次に、タイマーの処理もクラスにカプセル化してしまいましょう。その前に、以下のようなインターフェイスを用意しておきます。これは、後でepollがタイマーの経過を通知するための窓口となるものです:

// epollを使ってイベントを受信するクラスが実装するインターフェイス
public interface IEPollListener
{
    // epollで管理に使用するディスクリプタ
    int Identity { get; }
    // epollがイベントを受信した際に呼び出すコールバックのメソッド
    void OnRaised();
}

タイマークラスはこのインターフェイスを実装して、epollからのイベントを受信できるようにします:

internal abstract class Timer : Descriptor, IEPollListener
{
    [NativeValue("time.h")]
    private static readonly int CLOCK_MONOTONIC;
    [NativeValue("time.h")]
    private static readonly int TFD_NONBLOCK;

    protected Timer()
        : base(Interops.timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK))
    {
    }

    public void SetInterval(long nsec)
    {
        var tm = new timespec
        {
            tv_sec = (int)(nsec / 1_000_000_000L),
            tv_nsec = (int)(nsec % 1_000_000_000L)
        };
        var newValue = new itimerspec
        {
            it_value = tm,
            it_interval = tm
        };

        Interops.timerfd_settime(this.Identity, 0, ref newValue, out var dummy);
    }

    // epollで管理に使用するディスクリプタ
    int IEPollListener.Identity => this.Identity;

    // epollがイベントを受信した際に呼び出すコールバックのメソッド
    void IEPollListener.OnRaised()
    {
        // タイマーイベントを消化する
        Interops.timerfd_read(this.Identity, out var timerData,(UIntPtr)(sizeof(ulong)));
        // 派生クラスの実装を呼び出す
        Raised();
    }

    // タイマー経過を示す純粋仮想メソッド(継承して処理を実装)
    protected abstract void Raised();
}

タイマークラスは更に継承して、Raisedメソッドを実装することで、タイマー経過時の処理を簡単に記述できるようにしておきます。

C#で書き始めた途端に、ものすごく設計が捗るようになった気がしますね。最後にepollを処理するクラスを用意します。epoll自体もディスクリプタで管理されるので、Descriptorクラスを継承します:

public sealed class Application : Descriptor
{
    public Application()
        : base(Interops.epoll_create1(0))
    {
    }

    // 指定されたインスタンスをepollに登録する
    public void RegisterDescriptor(IEPollListener target)
    {
        // インスタンスの参照を固定する
        GCHandle handle = GCHandle.Alloc(target, GCHandleType.Pinned);

        // epollにインスタンスハンドルを結びつけるための情報
        var ev = new epoll_event {
            events = Interops.EPOLLIN,
            data = new epoll_data_t { ptr = GCHandle.ToIntPtr(handle) }
        };

        // 登録する
        Interops.epoll_ctl(
            this.Identity,
            Interops.EPOLL_CTL_ADD,
            target.Identity,
            ref ev);
    }

    // 指定されたインスタンスをepollから解除する
    public void UnregisterDescriptor(IEPollListener target)
    {
        // ...
    }

    public void Run()
    {
        while (true)
        {
            var ev = new epoll_event();
            var numEventsOccurred = Interops.epoll_wait(this.Identity, ref ev, 1, -1);

            if (numEventsOccurred == -1)
            {
                break;
            }
            if (numEventsOccurred == 1)
            {
                // GCHandle.ToIntPtrメソッドで得たポインタを使ってGCHandleを復元する
                GCHandle handle = GCHandle.FromIntPtr(ev.data.ptr);
                // GCHandleからインスタンス参照を入手する
                var target = (IEPollListener)handle.Target;
                target.OnRaised();
            }
        }
    }
}

この後で紹介しますが、このクラスはWinFormsやWPFのApplicationクラスに似た位置づけなので、同じようにApplicationクラスと名付けました。IEPollListenerインターフェイスを実装したクラスをepollに連携させるように登録・解除出来るように、RegisterDescriptorメソッドとUnregisterDescriptorメソッドを公開しています。そして、Runメソッドでepollイベントの監視を行います。

このコードには少し解説が必要でしょう。ポイントはGCHandle構造体です。この構造体はインスタンスへの参照をガベージコレクタの操作から保護します。

この記事ではあえてIL2Cの内部構造に触れてきませんでしたが、IL2Cはマークアンドスイープ方式のガベージコレクタを持っています。クラスのインスタンスをnewすると、そのインスタンスはヒープメモリ(最終的にはmalloc関数)に格納されます。マークアンドスイープ方式では、メモリコンパクションと呼ばれる操作は行われませんが、使われていないインスタンスについては自動的に解放(free関数)されます。

ここで問題が2つあります:

  • ネイティブAPIに、インスタンス参照のポインタを渡す方法
  • ネイティブAPIがそのポインタを操作する可能性がある時に、誤ってインスタンスを解放してしまわないようにする方法

本物の.NET CLRや.NET Core、monoではこれに加えて、インスタンスが別の場所に移動してしまうかもしれない問題もある(メモリコンパクションが発生)のですが、IL2Cではその問題は発生しません。これらの問題は、GCHandleを使えば解決できます。

まず、インスタンスが誤って解放されないようにするには、GCHandle.AllocメソッドをGCHandleType.Pinnedで呼び出します。これで指定されたインスタンスは、IL2C上で使用されていなくても破棄されなくなります。

次に、GCHandle.ToIntPtrメソッドを呼び出すと、Allocで指定したインスタンスへのポインタが得られます。ポインタの型はSystem.IntPtr型(C言語ではintptr_t型)です。このポインタは、基本的に直接操作するためのものではありません。間接的にインスタンスを示す値と考えておけばよいでしょう。

これで上記2点についてはクリアできました。但し、注意すべきことがあります。一度GCHandle.Allocでインスタンスを固定すると、解除するまでは永久にヒープメモリに保持され続けます。不要になったタイミングでGCHandle.Freeメソッドを呼び出して固定を解除する事を忘れないようにしましょう。メモリリークに繋がります(具体的な方法についてはUnregisterDescriptorメソッドを参照して下さい)。

上記で得たポインタは、epoll_event構造体の中のepoll_data_t構造体に保存します。この構造体はNativeType属性をつけてあります(実際は共用体です。IL2C/Invokeなら簡単に共用体を扱えます):

[NativeType("sys/epoll.h")]
internal struct epoll_data_t
{
    // C言語のepoll_data_t.ptrは、void*型
    public NativePointer ptr;
}

[NativeType("sys/epoll.h", SymbolName = "struct epoll_event")]
internal struct epoll_event
{
    public uint events;
    public epoll_data_t data;
}

ptrフィールドはNativePointer型で宣言してあります。実際の型がvoid*で宣言されていますが、C#ではvoid*は限られた使用法でしか使えません。NativePointer型としておくと、System.IntPtr型と自由に変換することが出来るようになります。

さて、epoll_wait関数がイベントを受信すると、そのイベントに対応する情報がepoll_event構造体に格納されます。これは、epoll_ctl関数で登録した情報と同じものです。つまり、イベントが発生すると、GCHandle.ToIntPtrメソッドで得たポインタを取得できることになります。このポインタはGCHandle.FromIntPtrメソッドを使ってGCHandleに復元できます。

GCHandleさえ手に入ってしまえば、Targetプロパティから(C#で認識可能な)インスタンス参照を得ることが出来ます。プロパティの型はSystem.Object型なので、明示的にIEpollListenerインターフェイスにキャストしておきましょう。

最後にIEPollListener.OnRaisedメソッドを呼び出せば、イベントを登録したときのインスタンスのOnRaisedが呼び出されます(ここではTimer.OnRaisedメソッド)。

これでインフラは全部揃いました。Blinkのサンプルとしての実装を含めた、Mainメソッドの実装です:

public static class Program
{
    private sealed class GpioBlinker : Timer
    {
        private readonly long[] blinkIntervals = new[] { 125_000_000L, 250_000_000L, 500_000_000L };
        private readonly GpioOutput output;
        private bool flag;
        private int blinkIntervalIndex;

        public GpioBlinker(int gpioId)
        {
            output = new GpioOutput(
                gpioId,
                GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
                true);
            this.NextInterval();
        }

        public override void Dispose()
        {
            base.Dispose();
            output.Dispose();
        }

        protected override void Raised()
        {
            output.SetValue(flag);
            flag = !flag;
        }

        public void NextInterval()
        {
            this.SetInterval(blinkIntervals[blinkIntervalIndex]);

            blinkIntervalIndex++;
            blinkIntervalIndex %= 3;
        }
    }

    private sealed class GpioPoller : Timer
    {
        private readonly GpioInput input;
        private readonly GpioBlinker blinker;
        private bool last;

        public GpioPoller(int gpioId, GpioBlinker blinker)
        {
            input = new GpioInput(gpioId);
            last = input.Value;
            this.blinker = blinker;
            this.SetInterval(100_000_000L);
        }

        public override void Dispose()
        {
            base.Dispose();
            input.Dispose();
        }

        protected override void Raised()
        {
            var current = input.Value;
            if (current != last)
            {
                if (!current)
                {
                    blinker.NextInterval();
                }
            }
            last = current;
        }
    }

    public static int Main()
    {
        using (var epoll = new Application())
        {
            using (var ledBlinker = new GpioBlinker(Interops.MT3620_RDB_LED1_RED))
            {
                using (var buttonPoller = new GpioPoller(Interops.MT3620_RDB_BUTTON_A, ledBlinker))
                {
                    epoll.RegisterDescriptor(ledBlinker);
                    epoll.RegisterDescriptor(buttonPoller);

                    epoll.Run();
                }
            }
        }

        return 0;
    }
}

GpioBlinkerクラスはLEDを点滅させるクラス、GpioPollerはボタン入力を検知してLEDの点滅速度を変化させるクラス、そしてMainメソッドでApplicationクラスと2つのクラスを生成して登録し、イベント処理のループに入ります。

今までインフラを作っていたので全容が見えにくかったのですが、このMainメソッドと2つのクラスだけを見れば、かなりシンプルでわかりやすいと思います。この図のように、Lチカ固有のコードにだけ関心を向けることが出来ます。

インフラの部分は、MT3620(又はAzure Sphere)のライブラリとして固めておけば、更に捗りそうです。NuGetパッケージ化すればなお良いでしょう。

これで完成です。このコードは ac2018-step2のタグ で保存してあります。

生成されたコードを比較する

最後に、IL2Cが生成するコードが、手でC言語コードを書いたものと比較してどの程度のフットプリントなのかを見てみます。

比較対象は、ビルドするとgccによって生成されるネイティブバイナリです(デプロイ用パッケージではありません)。例えば、$(OutDir)/Mt3620App.outです。なお、以下の全ての計測は、デバッグ情報を含んでいない(-g0)サイズです:

Debug Release
SDK Blinker (C sample code) 16KB 16KB
IL2C Step1 (Non OOP) 111KB 42KB
IL2C Step2 (OOP) 142KB 54KB

SDK BlinkerのサイズがDebugとReleaseで同一なのは、間違いではありません。それだけ、コードの複雑性がなく、最適化の余地は無かったということでしょう。IL2CのStep1・Step2共に、最適化がかかった場合のフットプリント縮小が劇的です。これはIL2Cの設計上狙ったとおりの結果です。

IL2Cは、比較的単調なコード(言い換えればC言語コンパイラが最適化を行いやすいコード)を出力し、かつ、gccのオプション(-fdata-sections -ffunction-sections -Wl,–gc-sections)によって、使用されていないシンボルをリンク時に削除する事を前提としています。

IL2Cのランタイムはまだ小規模ですが、やがてcorefxのマージを行うようになると、非常に大きくなることが予想されます。その場合でも、最低限必要なコードだけがバイナリに含まれて欲しいので、この前提が重要になります。

他にも、C言語標準ライブラリをできるだけ使用する、という方針もあります。独自のコードを含めば含んだだけ、フットプリントが増えてしまうからです。System.Stringの文字列操作も、内部ではC言語のwcslenやwcscatなどの関数を使ったり、マークアンドスイープガベージコレクタが、mallocやfree関数をバックエンドとして使っている(そしてコンパクションを行わない)のも、このような背景によるものです。

しかし、それでもSDK Blinkerのサイズと比較すると、やや大きいですね。不本意です。まだ努力が足りない感じです。フットプリントを小さくするアイデアは、まだ色々あるので、今後のバージョンでトライして行こうと思います。

まとめ

IL2Cを使って、Azure Sphere MCUを搭載したMT3620で、C#を使ってモダンなスタイルのコードを書くという試みを行いました。今回の試行でうまく動くようにするための技術的なフィードバックは、結構沢山出てきました(詳しくはコミットログを参照して下さい。かなり難易度高くて、初見では意味不明かもしれませんが…)。ドッグフーディングはやはりやっておくべきですね。

IL2Cのような試みは他でもいくつか行われていて、LLVM向けのものとか、Roslynの構文木からC言語のコードを生成するとか、はてはC#でブート可能なOSを作る、なんてものもあります(ググってみて下さい)。それらのコミュニティプロジェクトや、本家.NET Core・monoとの違いとしては、ポータビリティとフットプリントかなと思っています。まだまだ完成には遠い感じですが、これからも、ただの概念実験にならないようにしていきたいと思います。

PCいぢりメモ

訳あってPCの構成を変更する必要が生じたので、メモ。

CPUが一世代前なので、今更これを一から作るという人はいないと思うのですが、多分、この構成でやってる人はあまりいないと思うので、役に立つかも。気になるであろう部分を写真多めに盛っておきます。

構成

※Amazonでリンク張ってるので気になる方は直接ググって下さい

この構成の難しいところは、Mini-ITXなのでケース容積が非常に小さいところにハイエンドパーツを埋めているところです。ちっちゃいPC好きなので、どうしてもこういう無理ゲーな構成をやってしまう…

事の発端

朝電源を入れたらコレ。まぁ、SSDは壊れていなさそうと言うことと、前日Windows Updateで新たなIPが降ってきてた事もあり、それが直接の原因かも。ただ、折角(?)ブートしなくなってしまったので、とある問題の解決も含めて構成変更しようかと思いました。

というのも、SSD 2発をIntel RAID 0で構成してあったんですが、どうも感触が良くない。このSSD、体感かなり良いはずなのに、RAID 0構成してからあまり「速い」という印象もなく、なんだか勿体ないなと思ってました。それならRAID 0をやめて別のマシンにSSDを1台持って行ったほうが良いじゃないかと。

とはいえ、480GBだけでは心もとないので何か追加したい。それなら噂のNVMeに行くかと。しかしNVMeするならマザーボードも変えたほうが良く、この時点できちんとケースに収まるかかなり不安に…

最大の問題はクーラーで、現状のSAMUEL 17が新しいマザーに付くかどうか。念のため Cooler Master 風神スリム も手配。これは同じマザーのレビューで使われていたのが理由ですが、結局ダメでした(後述)。

マザーボード構築 (1)

WP_20160408_15_18_37_Proマザーボードだけさっくり置き換えるという訳にはいかず、一旦すべてのパーツをケースから外し、ケーブリングからやり直す事に。Mini-ITXの難しさの一つでもあるかな。で、取り出したマザーからCPUとメモリを外し、新しいマザーに移植したところ。

WP_20160408_15_19_01_Pro写っているヒートシンクはSAMUEL 17で、この時点で風神スリムは付かないことが判明。理由はメモリのヒートシンク高が高いこと。このマザーの特徴的なVRMの高さはぎりぎり収まるものの、メモリは完全にアウトでした。これは高さの様子です。

WP_20160408_15_19_21_Pro

WP_20160408_15_20_09_Proこういうことが買う前にわかる、良い施策無いかなーと思います。今はPCパーツショップがレポートとか書いてくれることが下支えになってると思うのですが、エッジの効いた構成だと事例がない…

WP_20160408_15_20_39_Proで、SAMUEL 17はちょっと変わったフットプリントなんですが、このヒートシンクは未来を予見しすぎているのか、買ってからずっと乗り換えで使えてるんですよね。今回もまさかこんなにフィットするとは思っていなかった。

WP_20160408_15_20_51_Proこれが….

WP_20160408_15_21_46_Proこう付きます。

WP_20160408_15_21_54_Pro

WP_20160408_15_22_30_Pro

WP_20160408_15_22_44_Pro

WP_20160408_15_23_01_Proもう、ホントギッチリで、SAMUEL天才か

WP_20160408_15_25_47_Pro大抵ここで心配になるのはEPS12Vコネクタの取り回しで、ほとんどのマザーはヒートシンク下にあるので先にコネクタを接続しておかなければならず、組み立てに苦労します。しかしこのマザーはそれを予見するかのように、EPS12Vを前方外周に配置していました。これ、かなりやりやすくて良いです。ASUSは昔トラブって以来避けてきたのですが、見直しました。

マザーボード構築 (2)

WP_20160408_15_29_18_Pro個人的にライザーカードの類は好きではない(どうしてもコネクターのような信頼性を損なう部品が必要になる)のですが、このマザーはライザーが3つもある。一つはVRM、そしてこのサウンドカード、最後にPCIe/Wifiカード。サウンドについては外付けのDACがあるので不要なのですが、これをつけておかないとバックパネルにみっともない穴が開いたままになるので、一応付けることに。

WP_20160408_15_30_55_Pro付けるとこんな感じ。

WP_20160408_15_34_47_ProWifiも不要なのですが、このライザーカードには、PCIeが刺さります。そして今回の構成変更の目的である、NVMeがここに刺さるので、やはりこれも使います。ただ、Wifiモジュールは外せそうだったので取り外しました。幸いWifiアンテナの穴(バックパネル)は出荷時で塞がっているので、サウンドカードのような問題はありませんでした。

WP_20160408_15_44_26_Pro_fこれが….

WP_20160408_15_45_03_Pro_fこう。

WP_20160408_15_54_50_Pro_fWifiはMini PCIeなので、外してオミットします。

WP_20160408_16_08_38_ProこのNVMe SSDは非常に高温になる事が知られているので、念のためヒートシンクを貼っておきました。サウンドカードとのクリアランスがちょっと不安になる感じです。間に何か挟んだほうが良いかもしれません。

WP_20160408_16_08_50_Pro

WP_20160408_16_09_06_Pro

組み込み

WP_20160408_16_14_08_Proさて、いざ組み込もうとしたら、なぜかきっちり入らない。よくよく見てみると、なんとVRMのライザーカードが共締め仕様となっており、ネジで止まってる…

WP_20160408_16_18_49_Proこのネジを外したところ、さっくりとハマりました。しかし、あらかじめ取り付けたファンが邪魔で一旦外す羽目に… 外してしまうとコネクタを再挿入するのが大変だ。

WP_20160408_16_30_37_Proケーブルを接続。良いですね、このきっちりかっちり感 :)

WP_20160408_16_33_16_Proファンのコネクタをどうにか接続し、取り付け完了(実はファンが逆であることに後で気が付く)。

WP_20160408_16_39_18_ProPCIeライザーカードとRadeonを取り付け、上部ファンも取り付ける。

上部ファンは12cmの穴が開いていますが、92mmのファンを無理やり付けています。理由はSFX電源の奥行きがやや長尺で、12cmだと干渉してしまうからです。この電源は650Wなんですが、これ以外に選択肢はなく、以前使っていた450WではRadeonを安定運用できなかったので、やむなくこういうことになっています。Mini-ITXは難しい…

WP_20160408_16_45_51_ProSSDとHDDを取り付けて…

WP_20160408_16_48_14_Pro

WP_20160408_16_49_26_Proどうにか、収まりました。

WP_20160408_16_50_44_ProSATAケーブルはスマートケーブルに変えないとダメですね。そのうちやります。

Overall

WP_20160409_11_22_48_Proファンの再換装は省略。

cdi折角なのでCristalDiskMarkで測ってみました。

cdm測定中、51℃まで上昇… 雫ちゃんに大音響でお願いされたのでびびった (*´Д`)

温度は使わなければすぐに冷えたので、ベンチマークをやり続けるとか極端な事をしなければ問題ないように感じました。

ここからVSやらOfficeやら再インストール。まだ先が長い…
ちなみにVectorは作業用として、HDDはデイリーバックアップ用として使います。

それでは。

不健康なIT戦士を健康的にするアレの話 – 第13回まどべんよっかいち勉強会

WP_20151010_13_18_22第13回まどべんよっかいち勉強会で、「不健康なIT戦士を健康的にするアレの話」というタイトルで登壇してきました。
今回の会場は、別の部屋でニコニコNTもやっていたので、ちょっとびっくりしました。

内容ですが、今回はIT技術の話からちょっとだけ離れて、健康の話をしてきました。たまにはこういう話も刺激になっていいんじゃないかな~的な感じです。こういう方面も、ちょっと掘り下げて調べると、色々なことが分かって面白いです。

今回のセッションはLTだったのを無理やりショートセッションぐらい使ってしまった(ごめんなさいごめんなさい)のですが、まだまだ盛り込んでいない要素があります。希望が多ければ、どこかで完全版をやろうかなと思っています。


オリジナルスライドはこちら: 不健康なIT戦士を健康的にするアレの話.pptx

第一回 三重合同懇親会 MGK2015

第一回 三重合同懇親会が、三重県津市で開催されたので行ってきました。

mgk2015
名古屋でクロスコミュニティな集いを「名古屋合同懇親会 NGK2014B」として開催していたのを、三重でもやりたい! というところから始まった企画で、Center CLRの紹介とLTの二本立てで参加しました。

参加人数は、26名と、会場に対して結構ぎっちりで、中部圏からちょっと離れた(失礼!)場所なのに、参加人数にびっくりしました。

この懇親会で分かったのは、「三重、IoTが熱い!」と言う事です。LTで伊勢方面のIT歴史を紹介していただいたのですが、伊勢発祥のデバイスメーカーって、結構多いんですね。前から伊勢方面は(理由がわからないけど)過熱しているというイメージがあって、実際に下地があるんだなという事が(良い意味で)意外でした。

で、いつもながら準備時間が十分取れなかったのですが、CenterCLRの紹介と、CenterCLR的ネタを三重にかましてきましたよ!!


Center CLRにおこしやす(誰

Center CLRの紹介です。特に何って事もないです… いや、興味ある方、ぜひぜひ参加どうぞ!!


Hello! Intermediate Language

これ、本当ならLTでやるネタでもないんですが、以前にがりっちさんがNawaTechで頑張ってLT枠でショートセッションネタをやってたのを見て、チャレンジしてみようと思ってたので、やってみました。

超絶まくってたので、理解のほどはどうかな?という感じですが、ちゃんとスタックマシンの話もしましたよー

セッションで説明したコードはGitHubにアップしてあるので、見たかった!という人は確認してみてください。

オリジナルスライド:Hello_Intermediate_Language
GitHub:CenterCLR.EmitLiveDemo


今日は.NET MicroFrameworkに積極的にかかわっている方との情報交換も出来て、IoT全般のコミュニティの取り組みとか、もっと大きな技術者的業界生き残り戦略がどーのこーのとか、物理演算シミュレーションがアレとか、主婦と子供のITへの関わりとか、想像もしなかった盛りだくさんで面白かったです。次回も参加しますよ!

それでは、また。

中華タブX89Winをアレする – 1円にもならない無駄な技術 Advent Calendar 2014

これは「1円にもならない無駄な技術 Advent Calendar 2014」の25日目の記事です。


Welcome中華タブ!

tablet1

それは忘れもしない、Microsoft Conference 2014の2日目、「MSC反省会」と称した呑み会の席の事。何だかごっつい「日本正規版 Windows Phone」と、もう一つ目に留まる自慢の一品を見た事から始まったのです。

これは、いわゆる中華タブレット「Teclast X89HD」。そこで目にした衝撃とは、7.9インチにて、2048×1536の超高解像度LCD、つまり「Retinaディスプレイ」だったのです。「iなんとか」、の世界ではもはや当たり前のこの液晶、Windows界隈では殆ど見かけません。そして何といっても、これならVisual Studioが捗るじゃありませんか!!!

tablet2

そう、それはつまり、こういう事なのです。

このサイズにしてこの解像度! むーん、欲すぃ…

しかし、このX89HD、この時点で再入荷の目途が立っていないとの事。翌日秋葉原に視察に行ったのですが、やはり売っていない orz
それ以来、再入荷されるのを心待ちにしていた所、とうとう販売開始、しかもマイナーバージョンアップして、「X89Win」として販売。速攻で予約、一週間程度で発送開始ですぐに来ました。

キタキタ、早速キーボードやマウスを接続し、BIOSがどんな風になっているのかを確認。おぉ、何だか普通のマザボBIOSみたいだ(AMI)。東芝タブレットのBIOSが本当に何も出来ないどころか、やたら使いにくいのに比べると、AMI BIOSで親近感があって良いです。もっとも、タッチパネルで操作出来ないので、BIOS触るときは必ずUSBキーボードが必要になります。

所でこのBIOS、設定可能項目が山のようにあります。それこそ、オーバークロッカー向けマザボの如く、です。これは面白そうだ、きっと何も考えずにconfiguration項目を全部有効にしているんじゃないだろうか?とか思いつつ、BIOS設定を色々弄っていて… あれ、起動しなくなった。


文鎮…

tablet3

もう、いきなりですよ。まったくダメ。電源長押しでも無反応。「これが噂の中華タブ洗礼か…」仕方が無いので、リセット的な何かが無いかと分解したのがこの写真です。分解してみたところ、半分ぐらいはバッテリー、もう半分がマザーボードという感じで、CPUと思わしき部分に広範囲の「ガム状のヒートシンク」が貼ってありました(写真では取り除いています)。

ちなみに、使い続けるなら分解はしない方が良いです。ツメがプラなので破壊しないで開けるのはかなり難しいです。無理にこじ開けた場合、ツメが緩くなって元通りキッチリとは閉まらなくなります。

しかし、マザーボード上を見ても、リセットジャンパーなどは見当たらず(あまり期待はしていなかったけど)、何か出来そうなものは皆無。空きスペースとして、3GモデムとかSIMスロットとオボシキパターンがあるので、今後3Gモデルが出るかも知れません。

tablet4

何かの参考になるかもしれません、アップの写真も撮っておきました。それにしても、中華タブの品質の悪さが揶揄されているものの、「ここまでの物をあの価格で実現してしまう」のは、かなりのインパクトがあるという印象でした。彼らはもっと製品の魅力にフォーカスすれば良いのに、とも思います。

さて、諦めきれなかったため、バッテリーを外して放電リセットを試みました。が、駄目でした。東芝タブでは放電してしまうと痴呆になって、最悪のやり直しが可能だったのですが、どうやらこれはフラッシュかEEPROMかにBIOS設定を記憶するようで、もう手詰まりな感じです。

結局年末近くで色々忙しかったこともあり、リプレースパーツと割り切って、諦める事にしました。そう、「リプレースパーツ」として。


Welcome中華タブ! アゲイン

tablet5

買っちまいました。もう一台。もう後に引けません。

さて、教訓から、怪しげなBIOS設定項目には触らない、と誓いを立て、まずはイメージバックアップします。この製品は、少し前に話題になったマイクロソフトの施策、「Windows 8.1 with Bing」の中文版がプリインストールされています。念のため、まずはこれのフルバックアップを取っておきます。バックアップは、日本語版で言う所のコントロールパネルの「回復」から、「回復ドライブの作成」で可能です。あらかじめUSBメモリ(32GB)を用意して臨みます。容量的には8GBでも行けるような気がします。手元に日本語版Windowsのマシンを置いておき、画面を比較しながらやれば選択肢を間違う事も無いでしょう。

そして、きれいさっぱり、Windows 8.1日本語版を入れなおします。プリインストールされたWindowsには日本語言語パックを追加する事が可能です。そうすれば、そのまま日本語で使い始める事が可能です。が、何しろ中華タブなので、何が入っているかわからない。真っ新にしておきたいというのが動機です。

wimboot

また、Visual Studio 2013とOffice 2013を使う事も目的となるので、内蔵ドライブ(eMMCで32GB)が手狭すぎます。素ではインストール出来ません。そこで、WIMBootという手法でこれらも全部インストールし、ディスクの空き領域を増やす作戦です。

WIMBootについては、「Windows 8.1 Updateの新機能「WIMBoot」を試す」が詳しいので参照してください。簡単に構造を説明すると、あらかじめ収めておきたいファイル群(WindowsのシステムファイルやOfficeやVisual Studio等)を圧縮イメージ(WIMイメージ)でeMMC内に保持し、個々のファイルはこのイメージ内の圧縮されたデータをポイントする事で、劇的に小さい容量での運用を可能にする機能です。

MSDNのWIMBootの解説は、不必要に面倒な事が書いてありますが、出来るだけ簡単にする方法として以下の手順を考えます。

  • X89Winは、32ビットOSだけがブート可能です。どうもUEFI BIOSが32ビットOSイメージしかブートしなくなっているようで、東芝タブが逆の64ビットのみだった事もあり、しばらくハマっていました。
  • 母艦PCでHyper-V上に、ゲストOSとしてWindows 8.1 EE 32bitをインストールします。Proなら同様の手順で出来るかもと思いますが、無印は分かりません。ディスク形式はvhdx、仮想マシン世代はバージョン1で構いません。インストール時には、新規に適当なローカルユーザーアカウントを作成します。
  • ゲストOSでAdministratorユーザーを有効化(既定では無効)します。
  • ゲストOSでAdministratorでログオンし直し、インストール直後に作ったローカルユーザーアカウントを削除し、プロファイルも削除します。
  • Windows Updateや、必要なアプリケーションなどをインストールします。Windows installerなどを使わないアプリだと、WIMBoot後に正しく環境が再現されないかもしれません。当然OfficeやVisual Studioなら問題ありません。ユーザー固有のレジストリ(HKCUなど)を使うアプリは、その定義が失われるので、駄目かも知れません。
  • 「C:\Windows\System32\sysprep」フォルダのsysprep.exeを起動します。
    C:\Windows\System32\sysprep> sysprep.exe /generalize /oobe /shutdown
    

    sysprep

    このコマンドの処理後は、自動的にゲストOSがシャットダウンし、次回起動時にはプレインストールされたWindowsのように、新品の初期状態から起動するようになります。Administratorで作られたプロファイルは、sysprepによって削除されていますので、余計な容量を使ってしまう心配はありません。
    なお、このゲストOSは、インストールから日数が経過すると、アクティベーション出来ていない状態として警告されるようになってしまいます。その状態ではsysprepのgeneralizeに失敗するため、その場合はゲストOSを作り直す必要があります(sysprepし終わったイメージは、起動しなければ問題ありません)。

  • これでテンプレートとなるイメージが生成出来たので、このvhdxをマウントして、そこからWIMBootイメージを作成します。

さて、このような手順で進めるのですが、一つ問題があります。果たして標準のWindows 8.1のドライバー群がどこまでデバイスを認識出来るのか?と言う事です。残念ながら、この中華タブ、デバイスが非標準的な物ばかりで、殆どNGという状態。素のWindowsでは全く使い物になりません。

  • タッチパネルNG
  • センサーNG
  • グラフィックはIntel HD Graphicsの筈だがNG
  • SDIOがNG
  • SDIOにぶら下がるRealtekのWifiがNG
  • BluetoothがNG
  • パワーマネージメントがNG
  • その他いろいろNG

ですが、X89Winのデバイスドライバー群はどこから入手すれば良いのかと。Teclastではマスターイメージはダウンロード可能なようですが、個別のドライバパッケージは無いようです。姉妹機のX98シリーズのドライバは公開されていたのですが、認識されないものが多かったです(これを試すのにもかなり時間を使ってしまった)。Intel純正のドライバにしても、BayTrail-T関係のドライバはどうも公開されていないように見えます。NUC当たりのドライバも試したのですが、INFアップデートが行けたぐらいで、殆ど解決に至りません。


やっと本題

このブログでは、プログラムの情報を発信している事もあり、こういったデバイスのレビューが主目的ではありません。そこで、稼働しているマシンやイメージからデバイスドライバを抽出する、というユーティリティプログラムを書いてみました。

GitHub: CenterCLR.ExtractDrivers
ツール: CenterCLR.ExtractDrivers-1.0.0.0.zip

このツールを、稼働状態にあるWindows 8.1(Windows 8.1にのみ対応)に対して使うと、そこにインストールされているドライバ(Windows標準のドライバを除く)をパッケージ毎に抽出してくれます。
例えば、Windowsのイメージ(*.wimをマウントした中身や物理的なファイル)が「D:\Images\x89win_windows\」配下にあり、いわゆる「Windowsフォルダ」がその下にある場合、以下のコマンドでそこからドライバ群を抽出します。

C:\WORK> CenterCLR.ExtractDrivers.exe D:\Images\x89win_windows\Windows

抽出されたドライバ群は、既定でカレントフォルダ配下の「DriverStore」に保存されます。注意点として:

  • INFファイル(*.inf)によって自動的にPnPインストールが可能なドライバだけが対象です。カスタム化された専用のインストーラーを使用するような、いい加減なドライバは抽出出来ません。Windows標準のドライバインストレーションに従っている必要があります。
  • マルチターゲットドライバ(32ビット・64ビット両対応)のドライバは、抽出に失敗するかも知れません。
  • 稼働状態と言っても、出来るだけオフラインで実行して下さい。X89Winで言うなら、Teclastからイメージをダウンロードして、その中に含まれている「install.wim」をdismでマウントして抽出するのが確実です。又はイメージバックアップしたものを母艦でマウントして、そこから抽出して下さい。動作中のWindowsから抽出する場合、ドライバファイルのコピー時にファイルのオープンに失敗する事があります。

さて、抽出と同時に、template.batを生成します。このファイルは、抽出したドライバ群を、WIMBootイメージに組み込むための「dismコマンド」のサンプルスクリプトです。これで、WIMBootイメージにドライバ群を組み込んでイメージを生成する事で、実機で起動時にドライバが自動的に組み込まれ、あたかも工場出荷時のように振る舞わせる事が出来ます。

この辺りの具体的かついい加減な説明 (;´Д`) や、使用したバッチファイル等は、GitHubに上げておいたので参考にして下さい。

最後に一点、X89Win固有の話ですが、センサーデバイスだけは自動認識されませんでした。理由は良く分かりませんがUMDFとしての認識に失敗しているようです(署名でエラーが出るので、これが原因かもしれません。多少不安ですが)。これはデバイスマネージャから「レガシーデバイスの追加」で「Kionix Sensor Fusion Device」を手動追加すると正しく認識されます。更に、レジストリエントリを追加する必要があります(GitHubにスクリプトをアップしてある「kxfusion.reg」ので使って下さい。これを入れないと、持ち替え時にディスプレイの方向が正しく変わりません)。

さぁ、これで心置きなくHack出来ますね!


あとがき

tablet6

このネタ、いつ書こうかと思っていたのですが、Qiitaで偶然に「1円にもならない無駄な技術」なるAdvent Calendarがある事を知って、エントリーしてみました。何と25日のトリを取ってしまって、このカレンダーを始めた方のイメージに合っているのかやや不安ですが、何かの参考になれば幸いです。

今年もあと少しですね。1円にもならない技術からでも、知識の発展はあるかなと思っています(むしろ)。来年も無駄な事をやりましょう!

素人の Per-Monitor DPI

先日、開発用のデスクトップPCを新調しました。その際に、話題の4KモニターDell UP2414Qを追加しました。

dell-up2414q-overview1

T221と比較してDisplayPortで接続出来るので、母艦のインターフェイスに悩まなくても済みます(実際は悩んだんですが、それはまた別の機会に)。また、60Hz行けるのが良い点です。

で、これをUXGAのL200Pと縦に並べました。

PerMonitorDPI1

下がUP2414Q、上がL200Pです。ここからが面白い。
Internet Explorerを下から上にドラッグして移動します。

PerMonitorDPI2

2台のモニターは解像度が違い過ぎるため、ドラッグ中のウインドウがL200P側ではみ出てしまっています。また、解像度が低い分、IE上のテキストや画像も物理的に大きく表示されています。ただ、違和感はありません。元々解像度が違う事を分かっているので、大きくなってしまうことは予想出来ます。そして…

PerMonitorDPI3

L200P側に半分以上?移動すると、このようになります。一瞬、「えっ?」と思うのですが、いきなりIE内のコンテンツが縮小され、しかし、ウインドウの外郭のサイズは変わらないため、妙に縮まって見えます。

これは、「Per-Monitor DPI」というやつかな。どれどれ、実測してみるか。

PerMonitorDPI4

UP2414Q

PerMonitorDPI5

L200P

少しサイズが違いますが、おおむね同じサイズとなるように、ピクセルが縮小されているようです。サイズが違うのは、L200PからDPIが供給されていない可能性があると思います。

さて、コンテンツだけを注目すると、非常に良い機能です。が、この「コレジャナイ」的な違和感… ウインドウの外郭も縮小されたらパーフェクトだったかもしれません。

… いやいや、本当にそうだろうか?

ウインドウの外郭クロームは、ずっと固定的なピクセルサイズを基準に運用されてきたので、今更これが変わるのは微妙かもしれません(見れないので分かりませんが)。

ウインドウの外郭に違和感を感じるのなら、ストアアプリならどうかなと思い、テストしてみました。

PerMonitorDPI6

UP2414Q

PerMonitorDPI7

L200P

これは素晴らしい!同じアプリケーションでありながら、極端に異なる解像度のモニターでも、表面的な違いはほとんどありません。そして、4Kモニターであれば、細かい文字も美しく描画されています。

PerMonitorDPI9

UP2414Q

PerMonitorDPI8

L200P

もちろん、デスクトップアプリにおいても「最大化」して使用すれば、ストアアプリとほぼ同じ結果が得られます(Per-Monitor DPIに対応していれば、です)。

このような事もあり、両方のモニターを同じ目的で使うつもりだったのですが、L200P側ではストアアプリ(主にNeuroniaとFacebook)を動かしたままで配置し、4Kの方で通常の作業を行う、という使い方に落ち着きました。2画面(又はそれ以上)のモニターをどれも同じ目的で使おうと考えている場合は、やはり同じモニターで揃えたほうが良さそうですね。

なお、Per-Monitor DPIについては、以下の記事が参考になると思います。

4大(?)ブラウザーの High DPI 対応 (だるやなぎさん)
アプリの高DPI(High DPI)対応について 第1回 ~ 高DPIとは ~ (田中さん)

Bar Windows 8.1 – BluetoothでGO!

Bar Windows 8 in 名古屋 with 8.1に登壇して、「BluetoothでGO!」というタイトルでセッションしてきました。

ご清聴ありがとうございました。プレゼンを置いておきます。
最後のデモで見せたコードはあまり整理されていません。反響があれば公開したいと思います。→ GitHubで公開しています。

BluetoothでGo!

次回も頑張ります (^^)

ThinkPad X201sで3G通信する

ThinkPad X201s

ThinkPadのよいところは、(保証に目をつむれば)カスタマイズの範囲が広い事だが、X系列は3G通信(WWAN)のオプションが搭載できる点でも良い。しかし、国内向けにはWWANオプションの選択肢が少ないか、あるいは提供されていない。

そんなわけで、どうすればそのようなX201sに3G通信の能力を与えられるか、というメモ。
(トライする場合は自己責任で、念のため)

  • QualComm Gobi2000 Mini PCIex moduleを買う。
    注意点は、ThinkPadの場合モジュールロックがあるので、ThinkPad用として販売されているモジュールしか使えない。例えば、Gobi2000はQualComm(Sierra)純正のモジュールが存在するが、これを取り付けるとBIOSがエラーを発してしまう。mod BIOSを入れるという選択肢もあるが、リスクを承知していない普通の人は手を出すべきではないだろう。特に純正モジュールは安かったりするので、飛びつかないように注意。
    もう一点は通信方式で、EVDO/EDGEタイプを買ってはいけない。国内の場合、DoCoMoかSoftBankに接続しなければならない。KDDIはEVDOの周波数帯は一致しているものの、KDDI側に機器の登録が必要らしいので、事実上使えない。
    今なら、Gobi3000やGobi4000(LTE)という選択肢もある。
    なお、これらモジュールは、技適マークがついている筈。
  • WWAN用無線アンテナを買う。
    国内向けThinkPadには、殆どのモデルでWWANアンテナが内蔵されていない。そのため、機種にあったWWANアンテナを入手する必要がある。ここで問題になるのが、アンテナの種類で、ヤフオクで売っているアンテナの大半は、GPSに対応していない。GobiシリーズはGPSにも対応しているのだが、こういったアンテナを買うと、WWAN通信は出来るがGPSは電波を拾えない。そんなわけで、GPSも使えるようにしたいのなら、Tycoのアンテナは多分ダメで、可能なら純正アンテナを入手するのが良い。
    X201sについての補足として、高解像度LCDを使っている場合は、そもそもWWANオプションは(世界的にも)存在しない。どうも天板の形状が通常LCDモデルと異なるようなので、仮に純正のアンテナを買うと、高解像度LCDモデルには取り付けできない可能性がある。
    なお、Gobi2000の場合は、アンテナは2系統必要。Gobi4000は、写真を見る限りでは3系統必要なようだ。私は、天板を外すのが面倒なので、3系統入れてしまった。Tycoのアンテナなので、GPSは使えない。取り付け位置は(幸いTycoは小型なので)適当に張り付けた。
  • SIMを入手する。
    国内では徐々に選択肢が増えてきた。手元で確認したのは、
    b-mobile ZSIM(使用しなければ0円のお手軽契約)
    DTI ServersMan SIM 3G 100(オプションなしで500円、100kbps)
    で、どちらもOKだった。ちなみに両方とも回線はDoCoMoを使う。

あとはドライバだが、Lenovo USA他の海外サイトから入手可能。変なドライバサイトからダウンロードしなければトラブルはないはず。これらはThinkVantageアプリケーションシリーズとして扱われており、Access Connection、GPSがある。但し、新品SIMのアクティベーションには以下のドライバが必要だが、この事はあまり書かれていないので注意(入れるだけでOK)。

Lenovo Mobile Broadband Activation (Windowsのバージョンが違う場合は探す事)

3GSIMを素で運用する場合、初めての場合は知らないかもしれないが、APNと呼ばれる識別子を設定する必要がある。b-mobileもDTIも説明書に書いてあるので、それに従ってAccess Connectionsのプロファイルを作ればよい。ユーザー名・パスワードも固定だが、忘れずに指定するように。

ACSettings1

ACSettings2

ACSettings3

晴れて接続できれば、以下のようになる。

Access Connection