みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

USB per-port power switching対応ハブガチャを引いてみた

はじめに

USBハブにはper-port power switching(PPPS)という機能に対応しているものがある。 これはポート毎に給電を制御するものであって、これが使えるとそのハブに接続されたUSB機器の電源をPCから制御することができる。

USBハブのポートが供給する電力量は小さいので、直接制御できるのはせいぜい小型のライトとかファンとかのようなものになるのだが、 USB連動電源タップなどと組み合わせると、それなりの機器の電源制御にも使用することができる。

Linuxが使える環境ではuhubctlというツールがあり、コマンドラインで制御することができる。

ただし、全てのUSBハブがPPPSに対応しているわけではない。 むしろ、PPPSに関してはUSBハブを提供するメーカーは全然気にしていないので、どのハブが対応しているかは買ってみるまでわからない。

今回は、ヨドバシカメラなどの量販店で3000程度で購入できるUSBハブをいくつか買ってみて、対応しているものがあるかを検証してみた。

結果を最初にいうと、1つ以外全部ハズレ。

その1つのアタリも4ポートのうち2ポートしか制御できないという微妙な結果となった。

検証方法

下記の手順で検証した。

  1. LinuxPCにUSBハブを接続
  2. uhubctlでUSBの制御チップが対応しているか確認
  3. 対応している場合は各ポートの電源を制御
  4. USBケーブルから電源線を引っ張り出し、テスタで電圧を計測

まず、制御チップが電源制御に対応しているかが鍵となる。そもそもこれに対応していないとPPPSは使用できない。

次に、制御チップが対応していても回路設計上ポートのVBUSが5Vに接続されているなどの場合は、 いくらチップを制御しても接続されている機器には5Vが供給されることになる。

PPPSはポートごとの給電を制御するため、ハブ自体の電源が落ちるわけではないのでこういうことがあり得る。 こればかりは USBハブの基板の設計次第なので本当に実際電圧を測ってみるまでわからない のだ。

uhubctlの対応状況

uhubctlの対応状況はリンク先の表に記載されている。

これはユーザーからの報告によって更新されている。この表には実際に制御してみて電圧が変化することが確認できたものしか追加されない。 つまり、この表に記載があるものは確実に使用できる。

しかし先述の通り、制御チップがPPPS対応でも基板の設計によって非対応のケースがあるため、VID:PIDが同じ別の機種が必ず対応しているとは言えない。

表を見てもらえばわかるが、記載があるハブは下記の点から入手性が高いとは言い難い。

  • 古いものが多い
  • 海外の製品が多い

今回の戦果

筆者がもともと持っていたものもついでに記載する。それ以外はヨドバシなどの量販店Amazonなどで購入した物。

もともと持っていたものは古いものが多かったので備考に手持ち品と記載し、今回の検証用の購入したものとは区別できるようにする。

ブランド 型番 ポート USB VID:PID チップ対応 ポート制御 備考
Digio2 UH-2404BL 4 2.0 05e3:0608 × ×
Buffalo BSH4U050U2 4 2.0 05e3:0608 × ×
Buffalo BSH4A110U3 4 3.0 0bda:0411 ×
Buffalo BSH4U06 4 2.0 05e3:0608 × × 手持ち品
Buffalo BSH4U01BK 4 2.0 05e3:0608 × × 手持ち品
Digio2 UH-3034W 4 3.0 05e3:0612 × ×
ELECOM U3H-A422BX 4 3.0 05e3:0626 × ×
Anker A7516 4 3.0 0bda:0411 ×
Buffalo BSH4A01 4 2.0 05e3:0608 × × 手持ち品
System TALKS USB2-HUB4X(SUGOI-HUB) 4 2.0 0409:005a Port1、2のみ
100均 ???? 4 1.0 0a05:7211 × ×

寸評

手持ち品も含めて何故かBuffaloに偏ってしまった。

05e3:0608Genesys Logic製のチップで、今回買った価格帯のUSBハブには、このチップが多い気がした。 これはチップ自体がPPPSに対応していないのでlsusbなどでこれが出たら確実にハズレ。 USB2.0だし仕方ないかな?と思いつつ、USB3.005e3:0626もPPPS非対応。 VID:05e3はuhubctlの対応表にも出てくるのでGenesys Logic=ハズレというわけではないだろうが、今回の検証結果のせいで印象が悪い。

いくつかのハブに搭載されていた0bda:0411Realtek製のチップで、チップとしてはアタリ。 しかし今回買ったハブの基板設計上の問題でハズレ扱いに。ちょっと不遇。

今回唯一のアタリのSUGOI-HUBは0409:005aNEC製のチップが搭載されている。安心の日本製! 「SUGOI-HUB(スゴイハブ)なんて仰々しい名前ついてんな。」くらい思っていたが、本当にスゴイやつだった。

100均のハブはVIDからしてUnknownというキレっぷり。いつ火を吹いてもおかしくない感じがやばい。

まとめ

今回は、外れた時の精神的&経済的なダメージが大きいので高額のハブは避けたが、低価格帯はことごとくハズレという結果になった。 結局買ってみるまでわからないっていうのは結構イタイ話だ。

中には高額なUSBハブを持っているひともいるだろうし、 Linuxが使える人は、ぜひともuhubctlで対応状況を調べて見てほしい。 そして対応しているものがあれば積極的にuhubctlのコミュニティに報告してほしい。 ツイッターなんかでつぶやくだけでも嬉しい。

USBハブのベンダの人たちはPPPS対応している製品に関しては積極的に宣伝してほしい。 購入するかどうかの決め手になってもおかしくないぐらい使えると便利な機能のはず。 そしてもっと対応製品作ってくれ。。。

USBハブで機器の電源制御ができたらいちいちリレーくまなくて良くなるしね。

Raspberry Pi Pico SDKでのFlashからのブートについて調べた

はじめに

私のベアメタルの心の師匠であるdwelch67さんが、さっそく何か作っていたのでパクる勉強するべく中身を眺めていた。

リンカスクリプトの下記の部分のLENGTH = 0xFCが引っかかったので調べてみた。

MEMORY
{
    flash : ORIGIN = 0x10000000, LENGTH = 0xFC
}

0xFCということは252バイトで、プログラムのサイズがこれしか書けないのだろうか?

READMEを読むと、252に関しては言及している。

This leaves 252 bytes for the second stage bootloader.

このサイズはthe second stage bootloaderがそうでなければならないということらしい。

最初の方をきちんと読むと下記のことがわかる。

  1. PicoはFlash-ROMから起動する場合先頭の256バイトをSRAMにコピーする。
  2. CRC32でデータをチェックし、正しい値が読めるまで繰り返す。
  3. 最終的に正しくCRCチェックが通らない場合は起動に失敗する。

RP2040のFlash-ROMブートシーケンス

RP2040 DatasheetFigure 15にブートシーケンスのチャートがある。

この部分が先ほどの記述の部分だとわかる。

f:id:mickey_happygolucky:20210210004214p:plain
CRCチェックのフロー

2.8.2.3.1. Checksumの下記の部分にチェックサムについて記述がある。

The last four bytes of the image loaded from flash (which we hope is a valid flash second stage) are a CRC32 checksum of the first 252 bytes.

CRCが最後の4バイトで、検査対象が先頭の252バイトということらしい。

dwelch67さんのこのコードはこのthe second stage bootloaderの部分でLチカをしてしまおうということっぽい(すごい)。

Pico SDKでの扱い

pico-examplesのblinkのように、Pico SDKでビルドされるアプリはどの様になっているのだろうか。

blinkのCMakeLists.txt

blinkのCMakeLists.txtを見てみる。

add_executable(blink
        blink.c
        )

# Pull in our pico_stdlib which pulls in commonly used features
target_link_libraries(blink pico_stdlib)

# create map/bin/hex file etc.
pico_add_extra_outputs(blink)

# add url via pico_set_program_url
example_auto_set_url(blink)

なるほど。わからん。

そもそもスタートアップルーチンやらリンカスクリプトなどはこの層には公開されていない。

SDKリンカスクリプト

Pico SDKリンカスクリプトがないか探してみる。

$ $ find -name '*.ld' | head
./src/rp2_common/boot_stage2/boot_stage2.ld
./src/rp2_common/pico_standard_link/memmap_copy_to_ram.ld
./src/rp2_common/pico_standard_link/memmap_blocked_ram.ld
./src/rp2_common/pico_standard_link/memmap_default.ld
./src/rp2_common/pico_standard_link/memmap_no_flash.ld

関係ありそうなのはこの5つ。boot_stage2.ldthe second stage bootloaderのことのようだ。つまりSDK自体はこのローダのコードを持っている。

boot_stage2.ld

boot_stage2.ldの中身を見てみる。LENGTH = 252で、これがFlashの先頭に書かれるプログラムと見て良さそう。

MEMORY {
    /* We are loaded to the top 256 bytes of SRAM, which is above the bootrom
       stack. Note 4 bytes occupied by checksum. */
    SRAM(rx) : ORIGIN = 0x20041f00, LENGTH = 252
}

SECTIONS {
    . = ORIGIN(SRAM);
    .text : {
        *(.entry)
        *(.text)
    } >SRAM
}

memmap_default.ld

memmap_default.ldを見てみる。

SDKでアプリを作る時にデフォルトで使用されるリンカスクリプトはこれっぽい。

...(snip)...

MEMORY
{
    FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k
    RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
    SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
    SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
}

ENTRY(_entry_point)

SECTIONS
{
    /* Second stage bootloader is prepended to the image. It must be 256 bytes big
       and checksummed. It is usually built by the boot_stage2 target
       in the Raspberry Pi Pico SDK
    */

    .flash_begin : {
        __flash_binary_start = .;
    } > FLASH

    .boot2 : {
        __boot2_start__ = .;
        KEEP (*(.boot2))
        __boot2_end__ = .;
    } > FLASH

    ASSERT(__boot2_end__ - __boot2_start__ == 256,
        "ERROR: Pico second stage bootloader must be 256 bytes in size")

...(snip)...

Flashの先頭に.boot2セクションが置かれるようになっていて、このセクションの先頭から末尾までのサイズが256バイトじゃないとエラーになる。

ビルドしてelfを作った時に、.boot2セクションにthe second stage bootloaderのコードが入るようになっているようす。

.boot2セクションについて

この.boot2セクションはどこから来るのか。

$ grep -r '\.boot2' .
./src/rp2_common/boot_stage2/pad_checksum:50:        ofile.write(".section .boot2, \"ax\"\n\n")
./src/rp2_common/pico_standard_link/memmap_copy_to_ram.ld:45:    .boot2 : {
./src/rp2_common/pico_standard_link/memmap_copy_to_ram.ld:47:        KEEP (*(.boot2))
./src/rp2_common/pico_standard_link/memmap_blocked_ram.ld:45:    .boot2 : {
./src/rp2_common/pico_standard_link/memmap_blocked_ram.ld:47:        KEEP (*(.boot2))
./src/rp2_common/pico_standard_link/memmap_default.ld:45:    .boot2 : {
./src/rp2_common/pico_standard_link/memmap_default.ld:47:        KEEP (*(.boot2))

pad_checksumで埋め込んでいるっぽい。これの中身を確認する。

#!/usr/bin/env python3

...(snip)...
try:
    with open(args.ofile, "w") as ofile:
        ofile.write("// Padded and checksummed version of: {}\n\n".format(args.ifile))
        ofile.write(".cpu cortex-m0plus\n")
        ofile.write(".thumb\n\n")
        ofile.write(".section .boot2, \"ax\"\n\n")
        for offs in range(0, len(odata), 16):
            chunk = odata[offs:min(offs + 16, len(odata))]
            ofile.write(".byte {}\n".format(", ".join("0x{:02x}".format(b) for b in chunk)))

pythonスクリプトCRCチェックサムの計算をしている。ついでに、対象ファイルに.boot2セクションとしてローダのコードとCRCを埋めているようだ。

このスクリプトはどこから実行されるのか。

$ grep -r 'pad_checksum'  .
./src/rp2_common/boot_stage2/CMakeLists.txt:48:            COMMAND ${Python3_EXECUTABLE} ${PICO_BOOT_STAGE2_DIR}/pad_checksum -s 0xffffffff ${ORIGINAL_BIN} ${PADDED_CHECKSUMMED_ASM}

ローダのCMakeLists.txtで下記のようになっている。

...(snip)...

    set(PADDED_CHECKSUMMED_ASM ${CMAKE_CURRENT_BINARY_DIR}/${NAME}_padded_checksummed.S)

...(snip)...

    add_custom_command(OUTPUT ${PADDED_CHECKSUMMED_ASM} DEPENDS ${ORIGINAL_BIN}
            COMMAND ${Python3_EXECUTABLE} ${PICO_BOOT_STAGE2_DIR}/pad_checksum -s 0xffffffff ${ORIGINAL_BIN} ${PADDED_CHECKSUMMED_ASM}
            )

...(snip)...

一度ビルドして、そのバイナリデータとCRCを含んだ.boot2のセクションの情報を${NAME}_padded_checksummed.Sとして出力している。

この出力結果を確認する。

// Padded and checksummed version of: /home/mickey/work/rpi_pico/pico/picoprobe/build/pico-sdk/src/rp2_common/boot_stage2/bs2_default.bin

.cpu cortex-m0plus
.thumb

.section .boot2, "ax"

.byte 0x00, 0xb5, 0x32, 0x4b, 0x21, 0x20, 0x58, 0x60, 0x98, 0x68, 0x02, 0x21, 0x88, 0x43, 0x98, 0x60
.byte 0xd8, 0x60, 0x18, 0x61, 0x58, 0x61, 0x2e, 0x4b, 0x00, 0x21, 0x99, 0x60, 0x02, 0x21, 0x59, 0x61
.byte 0x01, 0x21, 0xf0, 0x22, 0x99, 0x50, 0x2b, 0x49, 0x19, 0x60, 0x01, 0x21, 0x99, 0x60, 0x35, 0x20
.byte 0x00, 0xf0, 0x44, 0xf8, 0x02, 0x22, 0x90, 0x42, 0x14, 0xd0, 0x06, 0x21, 0x19, 0x66, 0x00, 0xf0
.byte 0x34, 0xf8, 0x19, 0x6e, 0x01, 0x21, 0x19, 0x66, 0x00, 0x20, 0x18, 0x66, 0x1a, 0x66, 0x00, 0xf0
.byte 0x2c, 0xf8, 0x19, 0x6e, 0x19, 0x6e, 0x19, 0x6e, 0x05, 0x20, 0x00, 0xf0, 0x2f, 0xf8, 0x01, 0x21
.byte 0x08, 0x42, 0xf9, 0xd1, 0x00, 0x21, 0x99, 0x60, 0x1b, 0x49, 0x19, 0x60, 0x00, 0x21, 0x59, 0x60
.byte 0x1a, 0x49, 0x1b, 0x48, 0x01, 0x60, 0x01, 0x21, 0x99, 0x60, 0xeb, 0x21, 0x19, 0x66, 0xa0, 0x21
.byte 0x19, 0x66, 0x00, 0xf0, 0x12, 0xf8, 0x00, 0x21, 0x99, 0x60, 0x16, 0x49, 0x14, 0x48, 0x01, 0x60
.byte 0x01, 0x21, 0x99, 0x60, 0x01, 0xbc, 0x00, 0x28, 0x00, 0xd0, 0x00, 0x47, 0x12, 0x48, 0x13, 0x49
.byte 0x08, 0x60, 0x03, 0xc8, 0x80, 0xf3, 0x08, 0x88, 0x08, 0x47, 0x03, 0xb5, 0x99, 0x6a, 0x04, 0x20
.byte 0x01, 0x42, 0xfb, 0xd0, 0x01, 0x20, 0x01, 0x42, 0xf8, 0xd1, 0x03, 0xbd, 0x02, 0xb5, 0x18, 0x66
.byte 0x18, 0x66, 0xff, 0xf7, 0xf2, 0xff, 0x18, 0x6e, 0x18, 0x6e, 0x02, 0xbd, 0x00, 0x00, 0x02, 0x40
.byte 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03, 0x5f, 0x00, 0x21, 0x22, 0x00, 0x00
.byte 0xf4, 0x00, 0x00, 0x18, 0x22, 0x20, 0x00, 0xa0, 0x00, 0x01, 0x00, 0x10, 0x08, 0xed, 0x00, 0xe0
.byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0xb2, 0x4e, 0x7a

なるほど。これをアプリを作る時にリンクしている。

まとめ

Raspberry Pi PicoでFlashから起動する際の仕組みを調べた。

  • RP2040はFlashからプログラムを起動する場合、最初に先頭の256バイトを読み込む
  • その際にCRCチェックされる
  • 通常では、CRCを除く先頭の252バイトのプログラムが残りをロードするためのローダであることが期待される
  • Pico SDKではboot_stage2として実装されている
  • Flashから起動するアプリケーションをビルドする時に.boot2セクションにこのコードが埋め込まれる

Raspbery Pi Pico Ubuntu20.04上にSDK環境構築

はじめに

ラズベリーパイPico(RPi Pico)が入手できた。

開発環境としてラズパイ4が必要とされているが実際のところそうでもなさそうなのでUbuntu 20.04に開発環境(SDK)を構築しLチカを動かしてみる。

pico_setup.shについて

C/C++の開発環境を構築するためにpico_setup.shというものが提供されている。

試しにUbuntu 20.04で動かしてみる。

$ wget https://raw.githubusercontent.com/raspberrypi/pico-setup/master/pico_setup.sh
$ chmod +x ./pico_setup.sh
$ ./pico_setup.sh

下記のようにエラーになる。

vscode.deb                                         100%[===============================================================================================================>]  57.86M  8.89MB/s    in 6.6s

2021-02-04 13:19:47 (8.82 MB/s) - `vscode.deb' へ保存完了 [60667760/60667760]

パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
注意、'./vscode.deb' の代わりに 'code:armhf' を選択します
インストールすることができないパッケージがありました。おそらく、あり得
ない状況を要求したか、(不安定版ディストリビューションを使用しているの
であれば) 必要なパッケージがまだ作成されていなかったり Incoming から移
動されていないことが考えられます。
以下の情報がこの問題を解決するために役立つかもしれません:

以下のパッケージには満たせない依存関係があります:
 code:armhf : 依存: libnss3:armhf (>= 2:3.26) しかし、インストールすることができません
              依存: apt:armhf しかし、インストールすることができません
              依存: libxkbfile1:armhf しかし、インストールすることができません
              依存: libsecret-1-0:armhf しかし、インストールすることができません
              依存: libgtk-3-0:armhf (>= 3.10.0) しかし、インストールすることができません
              依存: libxss1:armhf しかし、インストールすることができません
              依存: libgbm1:armhf しかし、インストールすることができません
E: 問題を解決することができません。壊れた変更禁止パッケージがあります。

しかしこれはARM環境向けのVSCodeをインストールしようとして依存関係が解決できないというエラーなので、エディタ以外のSDKのセットアップ自体は正しく終了している。

スクリプトを確認したところ、VSCodeのインストールのあとはラズパイのUARTの有効化処理なので、こちらも必ず実行しなければならないものではない。

PCを再起動するか下記のコマンドで環境変数を読み込みすると、SDKが使用できるようになる。

$ source ~/.bashrc

RPi PicoをPCと接続

ボード側のBOOTSELボタンを押しながら、USBケーブルでPCと接続すると自動的に/media/${USER}/RPI-RP2にマウントされる。

プログラムの書き込み

拡張子がuf2のファイルをRPI-RP2に書き込むと、ボードへの転送からリセットまで行われる。

つまり下記のような流れで処理が行われる。

  1. RPI-RP2へ書き込み
  2. PCからRPi Picoへプログラムを転送
  3. RPi Picoのリセット
  4. 書き込まれたプログラムが開始

Blink(Lチカ)

${PICO_EXAMPLES_PATH}/build/blinkblink.uf2が生成されているのでこれを先ほどマウントされたRPI-RP2にコピーする。

$ cp ${PICO_EXAMPLES_PATH}/build/blink/blink.uf2 /media/${USER}/RPI-RP2

オンボードの緑色のLEDがチカチカするようになる。

Hello World(USB-UART版)

${PICO_EXAMPLES_PATH}/build/hello_world/usbhello_usb.uf2が生成されているのでこれを先ほどマウントされたRPI-RP2にコピーする。

$ cp ${PICO_EXAMPLES_PATH}/build/hello_world/usb/hello_usb.uf2 /media/${USER}/RPI-RP2

ボード上でこのプログラムが実行されるとPCで/dev/ttyACMxというデバイスが検出されるようになる。

$ dmesg | grep ACM
3085:[166271.475390] cdc_acm 1-1:1.0: ttyACM0: USB ACM device

筆者の実行環境では/dev/ttyACM0が認識された。これをminicomなどの端末で開く。

$ minicom -D /dev/ttyACM0

すると次のように表示される。

minicom へようこそ 2.7.1

オプション: I18n 
コンパイルされた日時は:  Dec 23 2019, 02:06:26.
ポート /dev/ttyACM0, 14:59:27

CTRL-@ Z を押すと、説明画面になります。

Hello, world!
Hello, world!
Hello, world!
... (省略) ...

まとめ

Ubuntuでも問題なくRPi Pico向けのSDKを構築し、プログラムをボードに書き込むことができる。

pico_setup.shはVSCodeのインストールでエラーになるが、SDKのセットアップ自体は完了しているので問題はない。

CMakeでGoogleTest(gtest_add_tests vs gtest_discover_tests)

はじめに

CMakeにはCTestというテストランナーがある。

CTestはテスト用の実行ファイル(テストバイナリ)が1つのテストとして認識される。

GoogleTest(GTest)のようなテストフレームワークの場合、1つのテストバイナリに複数のテストケースが含まれることが常となる。

そこでCMakeにはGTestのテストが一つのCTestとして扱われるようにするためのサポート機能がある。

テスト対象のプログラム

CMake C++でユニットテスト入門(初級編)で作成した、なんの役にも立たないテストプログラムを使用する。

使用するのは下記のファイル。

  • hello.cpp
  • hello.h

GTest

aptでインストール

ExternalProjectとかいろいろあるが、まずそれ以外のことを試したいので、googletestのパッケージをaptでインストールする。

$ sudo apt install -y googletest

ちなみに作業環境はUbuntu 20.04

テスト

このサンプルプログラムに対するGoogleTestを以下のように作ってみる。

#include "gtest/gtest.h"
#include "hello.h"
#include <stdexcept>

Hello h;

/* This block will uncomment after.
TEST(HelloTest, NullPtr) {
    EXPECT_THROW(h.hello(nullptr), std::runtime_error);
}
*/

TEST(HelloTest, default_param) {
    EXPECT_EQ(h.hello(), "empty");    
}

TEST(HellsoTest, empty_string) {
    EXPECT_EQ(h.hello(""),  "empty");
}

TEST(HelloTest, normal_case) {
    EXPECT_EQ(h.hello("John Doe"), "Hello John Doe");
}

NullPtrのテストはあえてコメントアウトしておく。

CMakeLists.txt

まずは、テストランナーを使用せずに、GTestをリンクしたテストバイナリを生成する。

cmake_minimum_required(VERSION 3.10)

# To use the googletest
find_package(GTest REQUIRED)

# Add the executable for the testcase which is using googletest
add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

テストバイナリのビルド

この時点でディレクトリ構成は次のようになっている。

.
├── CMakeLists.txt
├── hello.cpp
├── hello.h
└── test_hello.cpp

次のようにしてビルドする。

$ mkdir build && cd build
$ cmake ..
# make -j $(nproc)

テストバイナリの実行

$ ./test_hello 
Running main() from /home/mickey/work/trash/googletest-release-1.10.0/googletest/src/gtest_main.cc
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from HelloTest
[ RUN      ] HelloTest.default_param
[       OK ] HelloTest.default_param (0 ms)
[ RUN      ] HelloTest.empty_string
[       OK ] HelloTest.empty_string (0 ms)
[ RUN      ] HelloTest.normal_case
[       OK ] HelloTest.normal_case (0 ms)
[----------] 3 tests from HelloTest (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 3 tests.

3つのテストがPASSしている。

ここまでで下準備完了。

gtest_add_tests

gtest_add_testsを試す。この機能はCMake 3.1の時点で追加されている

今の形になったのは3.9の頃らしい。

CMakeLists.txtの修正

次のようにしてgtest_add_testsを使用してみる。

cmake_minimum_required(VERSION 3.10)
project(hello)

# Enable the testing features.
enable_testing()

# To use the googletest
find_package(GTest REQUIRED)

# Enable the GoogleTest integration.
include(GoogleTest)

# Add the executable for the testcase which is using googletest
add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

# Add the test case use the old feature.
gtest_add_tests(TARGET test_hello)

ctestの実行

ctestでテストを実行してみる。

$ ctest
Test project /home/mickey/work/c_lang/gtest/build
    Start 1: HelloTest.NullPtr
1/4 Test #1: HelloTest.NullPtr ................   Passed    0.00 sec
    Start 2: HelloTest.default_param
2/4 Test #2: HelloTest.default_param ..........   Passed    0.00 sec
    Start 3: HelloTest.empty_string
3/4 Test #3: HelloTest.empty_string ...........   Passed    0.00 sec
    Start 4: HelloTest.normal_case
4/4 Test #4: HelloTest.normal_case ............   Passed    0.00 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) =   0.01 sec

コメントアウトされているはずのNullPtrテストが実行されている。

gtest_add_testsの弱点

一つのテストバイナリを指定すると、GTestのテストケースに対応してGTestを実行してくれるが、下記の問題点をはらんでいる。

  1. テスト抽出のタイミングがcmake実行時
  2. 文字列ベースでテストを抽出するため、コメントアウトされていることを区別しない

つまり、Cコンパイラは当然のように知っているコメント行や#if 0などは感知されないため、TESTマクロの行を愚直に検出してしまう。

また、テストの検出がcmake実行時であるため、テストケースを追加したり削除したりする場合、cmakeから実行し直す必要がある。

gtest_discover_tests

gtest_discover_testsを試す。この機能は3.10で追加されている

CMakeLists.txtの修正

次のようにしてgtest_discover_testsを使用してみる。

cmake_minimum_required(VERSION 3.10)
project(hello)

# Enable the testing features.
enable_testing()

# To use the googletest
find_package(GTest REQUIRED)

# Enable the GoogleTest integration.
include(GoogleTest)

# Add the executable for the testcase which is using googletest
add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

# Add the test case use the gtest feature.
gtest_discover_tests(test_hello)

ctestの実行

ctestでテストを実行してみる。

$ ctest
Test project /home/mickey/work/c_lang/gtest/build
    Start 1: HelloTest.default_param
1/3 Test #1: HelloTest.default_param ..........   Passed    0.00 sec
    Start 2: HelloTest.empty_string
2/3 Test #2: HelloTest.empty_string ...........   Passed    0.00 sec
    Start 3: HelloTest.normal_case
3/3 Test #3: HelloTest.normal_case ............   Passed    0.00 sec

100% tests passed, 0 tests failed out of 3

Total Test time (real) =   0.01 sec

コメントアウトされているNullPtrテストは実行されていない。

NullPtrコメントの削除

NullPtrのテストをアンコメントして実行してみる。

$ make
$ ctest
Test project /home/mickey/work/c_lang/gtest/build
    Start 1: HelloTest.NullPtr
1/4 Test #1: HelloTest.NullPtr ................   Passed    0.00 sec
    Start 2: HelloTest.default_param
2/4 Test #2: HelloTest.default_param ..........   Passed    0.00 sec
    Start 3: HelloTest.empty_string
3/4 Test #3: HelloTest.empty_string ...........   Passed    0.00 sec
    Start 4: HelloTest.normal_case
4/4 Test #4: HelloTest.normal_case ............   Passed    0.00 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) =   0.01 sec

cmakeせずに、makeでテストバイナリをビルドし直すだけでNullPtrのテストも実行されるようになった。

gtest_discover_testsの強み

  1. テスト抽出のタイミングがビルド時
  2. コンパイル時の情報をもとに抽出するため、コメントアウトされたテストなどは抽出されない

つまり、コメントや#if 0などははプリプロセッサコンパイラのルールに従って適切に処理されるため、明示的に実行したくないテストに関して実行されてしまうことがない。

また、テストの抽出がビルド時であるため、テストケースを追加したり削除したりする場合、cmakeから実行する必要がない。

まとめ

CMakeにはCTestとGoogleTestをうまく強調するための機能が提供されている。

gtest_add_testsgtest_discover_testsがあるが、 問答無用でgtest_discover_testsの方を使うべき

日本語の情報で検索するとgtest_add_testsが出てくる場合が多いので注意が必要。

参考

CMake C++でユニットテスト入門(初級編)

はじめに

C++でコードを書く時にユニットテストも書きたい。

CMakeにはテストを実行するための仕組みがある。

CTest

CTestはいわゆるテストランナーで、CMakeがサポートするテストランナーの中では一番シンプルなもの。 指定されたものを実行するだけのシンプルなもの。(CMakeに他にテストランナーはなさそう)

CTestに関してはCMake: CTestが詳しい。

テストプログラム

簡単なテストプログラムを作成して実際に動かしてみる。

まずはCTestも何も使用しない単純なプロジェクトを作る。

ファイルの構成は次のようなもの。

├── CMakeLists.txt
├── hello.cpp
├── hello.h
└── main.cpp

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello)

add_executable(${PROJECT_NAME} main.cpp hello.cpp)

hello.h

#ifndef HELLO_H
#define HELLO_H

#include <string>

class Hello {
public:
    std::string hello(const char* const p = "") const;
};

#endif //HELLO_H

hello.cpp

#include "hello.h"
#include <sstream>
#include <stdexcept>

std::string Hello::hello(const char *const p) const {
    if (p == nullptr)
        throw std::runtime_error("This method can not accept the nullptr.");
    if (*p == '\0')
        return "empty";
    std::stringstream ss;
    ss << "Hello " << p;
    return ss.str();
}

main.cpp

#include "hello.h"
#include <iostream>
#include <cassert>

int main() {
    Hello h;
    //assert(h.hello(nullptr) == ""); //This will be error, therefore commented out.
    assert(h.hello() == "empty");
    assert(h.hello("") == "empty");
    assert(h.hello("John Doe") == "Hello John Doe");
    std::cout << "done" << std::endl;
    return 0;
}

ビルドと実行

下記のコマンドでビルドする。

$ mkdir build && cd build
$ cmake ..
$ make -j $(nproc)

実行してみる。

$ ./hello 
done

問題なく終了する。

CTestを使ってみる

CMakeLists.txt

下記のように修正する。

cmake_minimum_required(VERSION 3.10)
project(hello)

add_executable(${PROJECT_NAME} main.cpp hello.cpp)

# Enable the testing features.
enable_testing()

# Add the test which is simply run the application.
add_test(NAME run_test COMMAND ${PROJECT_NAME})

enable_testing()でテスト機能を有効化して、CTestで実行可能なadd_testでテストを定義する。

CTestは単純なランナーなので、実はどんな実行ファイルでも指定可能となっている。 ここではサンプルプロジェクトとして作ったhelloが実行されるように指定した。

テストの実行

CTestで追加されたテストはctestコマンドやmake testで実行可能となっている。

下記はctestで実行した例。

$ ctest
Test project /home/mickey/work/c_lang/ctest/build
    Start 1: run_test
1/1 Test #1: run_test .........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec

100% tests passedとなっており、成功したということになっている。

成功/失敗の判定

前述の通りテストは成功したことになっている。CTestではどんな実行ファイルでも指定できると前述したが、 成功/失敗の判断はどの様にしているのか。 これは単純にプログラム(main関数)からの戻り値が0かどうかだけで判断している。

プログラムが異常終了した場合やmainから0以外が返されると失敗になる。

試しにmain.cppの戻り値を0にして実行してみる。

diff --git a/main.cpp b/main.cpp
index 2d870a8..1cc8fe7 100644
--- a/main.cpp
+++ b/main.cpp
@@ -9,5 +9,5 @@ int main() {
     assert(h.hello("") == "empty");
     assert(h.hello("John Doe") == "Hello John Doe");
     std::cout << "done" << std::endl;
-    return 0;
+    return 1;
 }

実行結果は下記の通り。

$ ctest
Test project /home/mickey/work/c_lang/ctest/build
    Start 1: run_test
1/1 Test #1: run_test .........................***Failed    0.00 sec

0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.01 sec

The following tests FAILED:
      1 - run_test (Failed)
Errors while running CTest

0% tests passedとなって、きちんと失敗することが確認できた。

実用的なテスト

さすがにassertを挟んだだけの、しかもprojectのメインプログラムを指定するというのは、実用的な例とは言えないので、 メインプログラムとは別にきちんとしたユニットテストを追加してみる。

acutest

今回はヘッダファイルのみで構成されており、導入が簡単なacutestを使ってみる。

導入方法はヘッダファイルをダウンロードして、テストのソースコードからインクルードするだけ。

$ wget https://raw.githubusercontent.com/mity/acutest/master/include/acutest.h

再配布について

acutestの再配布の扱いについては、ヘッダに必要なことは全部記述してあるので、そのまま使う分にはヘッダーを追加するだけで問題ないとのこと。

Q: Do I need to distribute file README.md and/or LICENSE.md?

A: No. The header acutest.h includes URL to our repo, copyright note and the MIT license terms inside of it. As long as you leave those intact, we are completely fine if you only add the header into your project. After all, the simple use and all-in-one-header nature of it is our primary aim.

テストの追加

test_hello.cppとして下記の内容で作成する。

#include "acutest.h"
#include "hello.h"
#include <stdexcept>

void test_hello() {
    Hello h;
    TEST_EXCEPTION(h.hello(nullptr), std::runtime_error);
    TEST_ASSERT(h.hello() == "empty");
    TEST_ASSERT(h.hello("") == "empty");
    TEST_ASSERT(h.hello("John Doe") == "Hello John Doe");
}

TEST_LIST = {
   { "hello", test_hello },
   { nullptr, nullptr }
};

main関数はacutest側で用意してくれる。

テスト用のソースコードが複数になるようなケースでは下記のようにする。

#define TEST_NO_MAIN
#include "acutest.h"

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hello)

add_executable(${PROJECT_NAME} main.cpp hello.cpp)

# Enable the testing features.
enable_testing()

# Add the executable for the acutest case.
add_executable(test_hello test_hello.cpp hello.cpp)

# Add the test which is run the acutest case.
add_test(NAME run_test COMMAND test_hello)

テスト用の実行ファイルを定義して、add_testでそのコマンドを実行するように定義し直す。

テストの実行

ctestを実行する。

$ mkdir build && cd build
$ cmake ..
$ make -j $(nproc)
$ ctest
Test project /home/mickey/work/c_lang/ctest/build
    Start 1: run_test
1/1 Test #1: run_test .........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec

100% tests passedとなり、テストは成功している。

まとめ

CMakeはCTestというテストランナーを持っている。単純なランナーであるため使用するテストフレームワークは限定されない。 もっというと実行されるテスト用の実行ファイルはテストフレームワークで作成されたものである必要もない。

さまざまなテストフレームワークが利用可能という点でこれは利点ではあるが、 逆にいうとテストが失敗した際にどのテストが失敗したかなどの、テスト結果に対する表示がおおざっぱだというデメリットもある。

CMakeはGoogleTestに特化したテストランナーの機能GoogleTestをCTestと組み合わせて使用するための機能も持っているので、GoogleTestを使用する場合にはそちらを使用すると良さそう。

PlatfromIOでArduino UNO + 2.4 TFT LCD Shield(ILI9341)を動かす

はじめに

SPI接続のILI9341のLCDが欲しくてこれを買ったがSPIではなく届いてみると8ビットパラレルのモジュールだったので絶望した。

きっとポチッとしたときは眠たかったんだと思う。

しかし買ってしまったものは仕方ないので動かしてみることにした。

PlatformIOのインストール

pipでインストールできる。

$ pip3 install platformio --user

ライブラリ

2.4 TFT LCD Shieldを駆動するためのライブラリはAdafruit-TFT-LCDを使う。

ライブラリのインストールはpio lib installで行う。依存関係もこのコマンドで解決される。

環境構築

ベース環境の構築

PlatformIOで環境を構築する。ライブラリのインストールもここで行う。

$ mkdir uno_r3
$ cd uno_r3
$ pio init -b uno
$ pio lib install "adafruit/Adafruit-TFT-LCD"

platformio.iniの修正

依存関係を追加しないとビルドが失敗するので、下記のようにする。

[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps = 
    adafruit/Adafruit-TFT-LCD@0.0.0-alpha+sha.9b701b6d5a
    adafruit/Adafruit BusIO
    Wire
    SPI

サンプルソースのコピー

graphicstestのサンプルをビルドするためソースをsrcディレクトリにコピーする。

$ cp .pio/libdeps/uno/Adafruit-TFT-LCD/examples/graphicstest/graphicstest.ino ./src

修正

このシールドはパチモンなので、チップのデバイスIDチェックの結果が残念なことになる。

そのため、IDを0x9341(ILI9341)に固定する必要がある。

--- graphicstest.ino~    2020-10-16 09:23:26.419496756 +0900
+++ graphicstest.ino  2020-10-16 09:28:54.187508970 +0900
@@ -57,7 +57,7 @@ void setup(void) {
 
   tft.reset();
 
-  uint16_t identifier = tft.readID();
+  uint16_t identifier = 0x9341;
 
   if(identifier == 0x9325) {
     Serial.println(F("Found ILI9325 LCD driver"));

ビルドおよび書き込み

次のコマンドでビルドする。

$ pio run

ターゲットへの書き込みは次のようにする。

$ pio run -t upload

まとめ

ILI9341(8ビットパラレル)のLCD ShieldをArduino UNOで試してみた。

パチモンのため、デバイスIDのチェックが機能しないためIDを固定する必要があるので注意。

それ以外は問題なく動いているようだ。

参考

YoctoProject CVE Checkについて調べる

はじめに

bitbakeではビルドするパッケージの脆弱性CVEによってチェックを行う機能がある。

使い方

local.confに下記を追加する。

INHERIT += "cve-check"

すると、各パッケージのタスクにdo_cve_checkが追加される。

bitbakeを実行すると、このタスクが実行され、未対策の脆弱性があった場合には警告として表示される。

実行例

この機能を実装しているcve-check.bbclassに記載されているコマンドの実行例を示す。

$ bitbake -c cve_check openssl
$ bitbake core-image-sato
$ bitbake -k -c cve_check universe

出力結果

CVEチェックの結果はログファイルとして保存される。

各パッケージに対する出力結果は${WORK_DIR}/temp/cve.logとして保存される。

イメージに対する出力結果はbuild/tmp/deploy/images/イメージ名.cveとして保存される。ラスベリーパイ4向けのcore-image-baseの場合はbuild/tmp/deploy/images/raspberrypi4/core-image-base-raspberrypi4.cveの様になる。

出力結果の例を下記に示す。

PACKAGE NAME: libvorbis
PACKAGE VERSION: 1.3.6
CVE: CVE-2020-20412
CVE STATUS: Patched
CVE SUMMARY: lib/codebook.c in libvorbis before 1.3.6, as used in StepMania 5.0.12 and other products, has insufficient array bounds checki\
ng via a crafted OGG file.
CVSS v2 BASE SCORE: 4.3
CVSS v3 BASE SCORE: 6.5
VECTOR: NETWORK
MORE INFORMATION: https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-20412

PACKAGE NAME: pixman
PACKAGE VERSION: 1_0.38.4
CVE: CVE-2013-0800
CVE STATUS: Unpatched
CVE SUMMARY: Integer signedness error in the pixman_fill_sse2 function in pixman-sse2.c in Pixman, as distributed with Cairo and used in Mo\
zilla Firefox before 20.0, Firefox ESR 17.x before 17.0.5, Thunderbird before 17.0.5, Thunderbird ESR 17.x before 17.0.5, SeaMonkey before \
2.17, and other products, allows remote attackers to execute arbitrary code via crafted values that trigger attempted use of a (1) negative\
 box boundary or (2) negative box size, leading to an out-of-bounds write operation.
CVSS v2 BASE SCORE: 6.8
CVSS v3 BASE SCORE: 0.0
VECTOR: NETWORK
MORE INFORMATION: https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-0800

適用済みのものはPatched、そうじゃないものはUnpatchedとなっている。

検出の仕組み

cvecheckerがベースとなっているらしい。

これは、オンラインからCVEのデータベースを取得し、SQLite3で脆弱性情報に対してマッチするものがあるかを検索するというもの。

プロダクト名とバージョンの組み合わせによって関係ある脆弱性があるかどうかを簡易的に判定するだけなので、このチェックによって全ての脆弱性に関する危険性を排除できるわけではないとされている。

Yoctoでの実装

do_cve_check

python do_cve_check () {
    """
    Check recipe for patched and unpatched CVEs
    """

    if os.path.exists(d.getVar("CVE_CHECK_DB_FILE")):
        try:
            patched_cves = get_patches_cves(d)
        except FileNotFoundError:
            bb.fatal("Failure in searching patches")
        whitelisted, patched, unpatched = check_cves(d, patched_cves)
        if patched or unpatched:
            cve_data = get_cve_info(d, patched + unpatched)
            cve_write_data(d, patched, unpatched, whitelisted, cve_data)
    else:
        bb.note("No CVE database found, skipping CVE check")

}

get_patches_cves(d)check_cves(d, patched_cves)によってチェックしているようだ。

get_patches_cves

レシピに含まれるファイル名にCVE-IDが含まれているか、ファイル中の文字列にCVE-IDが含まれているものは対策済みとして扱うようにしている。 どちらも正規表現による文字列マッチとなっている。

ファイル名は下記の条件でマッチしている。CVE-1234-211432やみたいなものが引っかかるようになっている。CVEに対する大文字/小文字の混在もOKのようだ。

cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)")

ファイル中の文字列は下記の条件でマッチしている。

cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+")

こちらはCVE: CVE-2020-13362のようなものが引っかかるようになっている。

check_cves

こちらはホワイトリストに含まれていれば無視し、それ以外のものをCVE_CHECK_DB_FILEで指定されているデータベースで検索する。

ホワイトリストには下記の2種類がある。

  • CVE_CHECK_WHITELIST(CVE-ID)
  • CVE_CHECK_PN_WHITELIST(パッケージ名)

CVE_CHECK_WHITELISTは特定のCVEを無視したい場合にCVE-IDを指定する。 CVE_CHECK_PN_WHITELISTは特定のパッケージに関するCVEを無視したい場合にパッケージ名を指定する。

データベースの検索条件はCVE_PRODUCTCVE_VERSIONの組み合わせとなっている。

CVE_PRODUCTはデフォルトではBPNが使用される。CVEデータベース上のプロダクト名とBPNがマッチしない場合や、1つのレシピで複数のパッケージを生成する場合などは明示的に設定する必要がある。

flacのレシピでは下記のように設定されている。

CVE_PRODUCT = "libflac flac"

CVE_VERSIONはデフォルトではPVが使用される様になっている。こちらもCVEデータベース上のバージョン表記と一致しない場合などは明示的に設定する必要がある。

CVE_CHECK_DB_FILEはSQLite3のデータベースとなっており、筆者が実行した環境では「"${DL_DIR}/CVE_CHECK/nvdcve_1.1.db"」が指定されている。 このファイルはcve-update-db-nativeによりダウンロードされる。

CVE対策をレシピに含める場合

get_patches_cvesのロジックにより、CVE対策としてパッチを作成する場合、ファイル名を下記のようにするか、

CVE-2020-12723.patch

コメント中に下記のような文字列を含めるかすると、対策済みとして扱われるようになる。

CVE: CVE-2020-13362

まとめ

bitbakeではCVE情報によって脆弱性をチェックする機能がある。

基本的にはプロダクト名とバージョンによって、関係あるCVEが対策済みかどうかを判断する。 レシピ中のSRC_URIで指定されたファイル名やコメントなどによって、データベース上では未対策のCVEでも、対策済みと扱うことができるようになっている。

また、特定のCVEや特定のパッケージをホワイトリストに追加することによって、未対策のものであっても問題ないものに関しては明示的にチェックしないようにすることもできる。

基本的には「プロダクト名+バージョン」や、ファイル名やコメントなどによる簡易的なチェックであり、 これらのチェック機能によって全ての脆弱性を網羅できるわけではないが、これで得られた情報をもとに脆弱性に対する方針を立てることができる。