みつきんのメモ

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

Ubuntu 18.04にtinygoをインストールする(2019/04編)(4/7追記)

はじめに

Ubuntu18.04でtinygoをインストールしようとしたらハマったのでメモっておく。

基本はここの手順。

Requirement

  • Go 1.11+
  • LLVM 7 (for example, from apt.llvm.org

あとは、ターゲットのツールチェイン。

  • ARM Cortex-M
  • AVR(Arduino)
  • WebAssembly

詳細は先述のリンク元を参照。

筆者環境はarm-none-eabiがインストール済み。

goのインストール

いろいろ手段があるが、インストーラによるインストールが一番便利だった。

$ sudo apt install wget git
$ wget -q https://storage.googleapis.com/golang/getgo/installer_linux
$ chmod +x installer_linux
$ ./installer_linux -version 1.11.7

GOPATHの設定は.bash_profileに自動的に作成される。

このインストーラ意外と優秀で-version 1.11.7とかやると、特定のバージョンをインストールしてくれる。

引数なしの場合は最新バージョン。今回は1.12がインストールされた。

(4/7 追記)

ここで1.11系のgoをインストールしておく必要がある。現時点では1.11.7が最新。

そうしないと、tinygoの実行時に次のエラーが発生する。

$ tinygo flash -target=bluepill examples/blinky1
.go/src/syscall/js/func.go:74:3: todo: unknown expression: select blocking []

既にインストールしてしまった場合はgoを入れ直す必要がある。 このインストーラでインストールした場合は~/.goを削除すれば良い。

$ rm -rf ~/.go
$ ./installer_linux -version 1.11.7

GOPATHの設定

UbuntuGUI環境では、.bash_profileはログイン時にしか読み込まれないので、 一度ログインし直すか、新しく開いた端末で次のコマンドを実行する。

$ source ${HOME}/.bash_profile

.bash_profileに追加された内容を.bashrcに移動してしまうというのも手。

export PATH=$PATH:/home/mickey/.go/bin

export GOPATH=/home/mickey/go

export PATH=$PATH:/home/mickey/go/bin

llvm-7のインストール

ここを参考にした。

sources.listの追加

llvm 7.0.1をインストールするために、/etc/apt/sources.list.d/llvm.listを次の内容で作成する。

# i386 not available
deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic main
deb-src http://apt.llvm.org/bionic/ llvm-toolchain-bionic main
# 7
deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-7 main
deb-src http://apt.llvm.org/bionic/ llvm-toolchain-bionic-7 main
# 8
deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-8 main
deb-src http://apt.llvm.org/bionic/ llvm-toolchain-bionic-8 main

これがないと、7.0.0がインストールされる。

$ wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|sudo apt-key add -
$ sudo apt update

これも忘れずに。

パッケージのインストール

Install (stable branch)に書いてあったものすべて入れた。

$ sudo apt install clang-7 lldb-7 lld-7
$ sudo apt install libllvm-7-ocaml-dev libllvm7 llvm-7 llvm-7-dev llvm-7-doc llvm-7-examples llvm-7-runtime
$ sudo apt install clang-7 clang-tools-7 clang-7-doc libclang-common-7-dev libclang-7-dev libclang1-7 clang-format-7 python-clang-7
$ sudo apt install lldb-7 lld-7 libc++-7-dev libc++abi-7-dev 

tinygoのインストール

まずはgo getを実行する。が、これは必ず失敗する(した)。

$ go get -u github.com/tinygo-org/tinygo

エラー発生(その1)

次のようなエラーが発生した。

$ go get -u github.com/tinygo-org/tinygo
# tinygo.org/x/go-llvm
../../go/src/tinygo.org/x/go-llvm/analysis.go:17:10: fatal error: llvm-c/Analysis.h: No such file or directory
 #include "llvm-c/Analysis.h" // If you are getting an error here read bindings/go/README.txt
          ^~~~~~~~~~~~~~~~~~~
compilation terminated.

ここによると、次のように説明されている。

If you get an error like this::

/usr/local/go/pkg/tool/linux_amd64/link: running g++ failed: exit status 1
/usr/bin/ld: error: cannot find -lLLVM-7
cgo-gcc-prolog:58: error: undefined reference to 'LLVMVerifyFunction'
cgo-gcc-prolog:80: error: undefined reference to 'LLVMVerifyModule'
[...etc...]
Or like this::

../go-llvm/analysis.go:17:93: fatal error: llvm-c/Analysis.h: No such file or directory
 #include "llvm-c/Analysis.h" // If you are getting an error here read bindings/go/README.txt

It means something is wrong with your LLVM installation. Make sure LLVM 7 is installed (Debian package llvm-7-dev). If it still doesn’t work, you can try running:

cd $GOPATH/src/github.com/tinygo-org/go-llvm
make config
And retry:

go install github.com/tinygo-org/tinygo

llvm-7-devはインストールしてあるので、make configを実行する。

go-llvmのソースの場所が実際は異なっているので次のようにする。

$ cd $GOPATH/src/tinygo.org/x/go-llvm/
$ make config

アドバイス通りに次を実行する。

$ go install github.com/tinygo-org/tinygo

エラー発生(その2)

今度は次のようなエラーが発生した。

# tinygo.org/x/go-llvm
panic: overlapping edits: [8156,8319)->"func() _Ctype_LLVMMetadataRef{ _cgo0 := /*line :297:3*/d.ref; var _cgo1 *_Ctype_char = /*line :298:3*/name; var _cgo2 _Ctype_size_t = _Ctype_size_t(len(t.Name)); var _cgo3 _Ctype_uint64_t = _Ctype_uint64_t(t.SizeInBits); var _cgo4 _Ctype_LLVMDWARFTypeEncoding = _Ctype_LLVMDWARFTypeEncoding(t.Encoding); _cgoCheckPointer(_cgo0); return _Cfunc_LLVMDIBuilderCreateBasicType(_cgo0, _cgo1, _cgo2, _cgo3, _cgo4); }()", [8299,8312)->" /*line :302:3*/_Ctype_LLVMDIFlags /*line :302:16*/"

goroutine 1 [running]:
cmd/internal/edit.(*Buffer).Bytes(0xc0000e1650, 0xc0001b20e0, 0x5f171f, 0xe)
    /usr/local/go/src/cmd/internal/edit/edit.go:79 +0x5a4
main.(*Package).writeOutput(0xc0000b4000, 0xc0000a63c0, 0x7ffe1ab7ad12, 0x35)
    /usr/local/go/src/cmd/cgo/out.go:562 +0x38e
main.main()
    /usr/local/go/src/cmd/cgo/main.go:356 +0xceb

これは解決方法も示されていなかったので、こまった。

どうやらgo-llvmでエラーが発生しているらしい。

解決方法

go-llvmのソースはgitリポジトリなので、コミットログを調べてみた。

commit 7707ae5d1261a8929edea7336c8087ca8b520d8d (HEAD -> master, origin/master, origin/llvm8, origin/HEAD)
Author: Ayke van Laethem <aykevanlaethem@gmail.com>
Date:   Thu Feb 14 11:10:53 2019 +0100

    Switch to LLVM 8

なんと、(現時点の)最新のコミットで LLVM 8に移行した と。

気を取り直してブランチを確認してみる。

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/llvm7
  remotes/origin/llvm8
  remotes/origin/make-release
  remotes/origin/master
  remotes/origin/windows

llvm7系のブランチはあるみたい。

tinygoのgo getでリポジトリが汚れているようなので一度きれいにする。

$ git reset --hard HEAD

llvm7ブランチに切り替える

$ git checkout -b llvm7 origin/llvm7

ビルドしてみる。

$ go build -v  github.com/tinygo-org/tinygo
tinygo.org/x/go-llvm
github.com/tinygo-org/tinygo/ir
github.com/tinygo-org/tinygo/interp
github.com/tinygo-org/tinygo/compiler
github.com/tinygo-org/tinygo

通った。

インストール

$ go install github.com/tinygo-org/tinygo

成功した!

まとめ

go-llvmLLVM 8に移行していた。 回避するには手でブランチを切り替える必要がある。

go getでは依存するリポジトリのコミットかブランチを指定できればよいのに。。

(4/7 追記) goのバージョンは1.11系(現時点の最新は1.11.7)をインストールする必要がある。

参考

Yocto gdbserverでリモートデバッグ

はじめに

gdbserverによるリモートデバッグ環境を今まで真面目に作ったことがなかったので調べてみた。

詳細な手順はここで紹介されている。

ターゲットはラズベリーパイ3。

この方法は、ターゲットにインストールされている既存のアプリケーションのデバッグに役立つ。

環境構築

いつものやつ。

INITはsystemdで。

ソース取得

$ mkdir -p rpi-thud/layers
$ cd rpi-thud/layers
$ git clone git://git.yoctoproject.org/poky.git -b thud
$ git clone git://git.yoctoproject.org/meta-raspberrypi -b thud
$ git clone git://git.openembedded.org/meta-openembedded -b thud
$ cd ../

環境変数設定

$ source layers/poky/oe-init-build-env build

自動的にビルドディレクトリに移動される。 これで、bitbake関連のツールが使用可能になる。

レイヤ追加

$ bitbake-layers add-layer ../layers/meta-openembedded/meta-oe
$ bitbake-layers add-layer ../layers/meta-openembedded/meta-python
$ bitbake-layers add-layer ../layers/meta-openembedded/meta-networking
$ bitbake-layers add-layer ../layers/meta-raspberrypi/

local.confの修正

local.confを次のように修正する。

MACHINE = "raspberrypi3"
DL_DIR ?= "${TOPDIR}/../downloads"

# enable uart(optional)
ENABLE_UART = "1"

# systemd
DISTRO_FEATURES_append = " systemd pam"
VIRTUAL-RUNTIME_init_manager = "systemd"
DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"
VIRTUAL-RUNTIME_initscripts = ""

# debugfs
IMAGE_GEN_DEBUGFS = "1"
IMAGE_FSTYPES_DEBUGFS = "tar.bz2"

# gdbserver
IMAGE_INSTALL_append = " gdbserver"

# debug build
DEBUG_BUILD = "1"

# network 
IMAGE_INSTALL_append = " connman connman-client"
EXTRA_IMAGE_FEATURES += "ssh-server-openssh"

ビルド

$ bitbake core-image-base
$ bitbake core-image-base -c populate_sdk

SDKのインストール

できあがったpoky-glibc-x86_64-core-image-base-cortexa7t2hf-neon-vfpv4-toolchain-2.6.2.shを実行しSDKをインストールする。

$ tmp/deploy/sdk/poky-glibc-x86_64-core-image-base-cortexa7t2hf-neon-vfpv4-toolchain-2.6.2.sh

ターゲットを起動する

できあがったcore-image-base-raspberrypi3.rpi-sdimgをマイクロSDカードに書き込み、ラズベリーパイ3を起動する。

$ sudo dd if=tmp/deploy/images/raspberrypi3/core-image-base-raspberrypi3.rpi-sdimg of=/dev/sdX bs=100M

/dev/sdXは環境に応じてsdbsdcに変更する。

debugfsの設定

デバッグに必要なシンボルファイルなどを完全に含んだsysrootを作成するには、debugfsを使用する必要がある。 ここでいうdebugfsカーネル機能のdebugfsとは異なり、デバッグ情報を含んだファイルシステムイメージのことを指す。

debugfsを生成するにはlocal.confに次の行を追記する。

IMAGE_GEN_DEBUGFS = "1"
IMAGE_FSTYPES_DEBUGFS = "tar.bz2"

debugfsはデバッグ情報のみを含んだいわばフラグメントなので、単体で使用しても意味がない。 使用するには次のようにする。

$ mkdir debugfs
$ cd debugfs
$ tar xvf ../build/tmp/deploy/images/raspberrypi3/core-image-base-raspberrypi3.tar.bz2
$ tar xvf ../build/tmp/deploy/images/raspberrypi3/core-image-base-raspberrypi3-dbg.tar.bz2

populate_sdkで作成したツールチェインをインストールした場所にもデバッグ情報を含んだsysrootが存在するが、 ターゲット上のすべてのファイルが含まれているわけではない。 SDKを使用して新規に開発したアプリケーションをデバッグするのには十分だが、それ以外のターゲット上のアプリケーションをデバッグするときには debugfsを含んだ完全なファイルシステムのイメージが必要となる。

デバッグ手順

次のような手順になる。

  1. ターゲットでgdbserverを起動
  2. ホストPCでgdbを起動

試しにlsコマンドをデバッグしてみる。

ターゲット側

$ gdbserver localhost:1234 /bin/ls

ホストPC側

SDKに含まれるgdbを使用する場合は次のようになる。

$ source /opt/poky/2.6.2/environment-setup-cortexa7t2hf-neon-vfpv4-poky-linux-gnueabi
$ cd debugfs/../ #debugfsの1つ上の階層
$ arm-poky-linux-gnueabi-gdb
(gdb) set sysroot debugfs
(gdb) set substitute-path /usr/src/debug debugfs/usr/src/debug
(gdb) target remote ターゲットのIPアドレス:1234

この時点からデバッグ可能となる。

次のようにデバッグする。

(gdb) b ls_main
(gdb) c 

これで(busyboxでの)lsコマンドのエントリポイントでブレークする。

gdbgui

gdbguiを使用する場合は次のようにする。

$ gdbgui -g arm-poky-linux-gnueabi-gdb

デバッグビルドについて

強い最適化がかかっている場合だと、シンボルデバッグ時におかしな挙動に見えることがある。 DEBUG_BUILDを有効化すると、bitbake実行時の最適化を抑えることができる。

local.confに次の行を追加する。

DEBUG_BUILD = "1"

これがない場合はTARGET_CFLAGSは次のようになる。

export TARGET_CFLAGS=" -O2 -pipe -g -feliminate-unused-debug-types -fdebug-prefix-map=/home/mickey/work/yocto/rpi-thud/build/tmp/work/raspberrypi3-poky-linux-gnueabi/core-image-base/1.0-r0=/usr/src/debug/core-image-base/1.0-r0 -fdebug-prefix-map=/home/mickey/work/yocto/rpi-thud/build/tmp/work/raspberrypi3-poky-linux-gnueabi/core-image-base/1.0-r0/recipe-sysroot= -fdebug-prefix-map=/home/mickey/work/yocto/rpi-thud/build/tmp/work/raspberrypi3-poky-linux-gnueabi/core-image-base/1.0-r0/recipe-sysroot-native= "

DEBUG_BUILD= "1"の場合のTARGET_CFLAGSは次のようになる。

export TARGET_CFLAGS=" -O -fno-omit-frame-pointer -g -feliminate-unused-debug-types -fdebug-prefix-map=/home/mickey/work/yocto/rpi-thud/build/tmp/work/raspberrypi3-poky-linux-gnueabi/core-image-base/1.0-r0=/usr/src/debug/core-image-base/1.0-r0 -fdebug-prefix-map=/home/mickey/work/yocto/rpi-thud/build/tmp/work/raspberrypi3-poky-linux-gnueabi/core-image-base/1.0-r0/recipe-sysroot= -fdebug-prefix-map=/home/mickey/work/yocto/rpi-thud/build/tmp/work/raspberrypi3-poky-linux-gnueabi/core-image-base/1.0-r0/recipe-sysroot-native=  -pipe"

-O2-Oになり、-fno-omit-frame-pointerがついている。これによってデバッグ情報が失われないようになっている。

まとめ

SDKを使用して新規に開発したアプリケーションをデバッグするにはSDKに含まれるsysrootのターゲット側のディレクトリで間に合うが、 既存のアプリケーションや、実機環境に近い状態でインテグレートした後のアプリケーションをデバッグする際には、 debugfsを使用する必要があることがわかった。

pandoc + reveal.jsの使い方

はじめに

2年ほど前にインターフェースオフ会の発表資料を作成した時にpandoc + reveal.jsを使ったが、使い方をまとめていなかったので思い出しがてらまとめる。

ただ、この時の発表では会場でノートPCが壊れて起動しないという散々な状況だったので、私としては苦い思い出。

環境の作成

ディレクトリ構成

reveal.jsのディレクトリとスライドのマークダウンは同じフォルダに置いておく。

work
├── input.md
└── reveal.js

イメージがある場合はこんな感じにしておくと便利だと思う。

work
├── input.md
├── images
│   └── imagefile.jpg
└── reveal.js

reveal.jsのダウンロード

$ git clone https://github.com/hakimel/reveal.js.git

スライドの生成

マークダウンを置いたディレクトリでpandocを実行する。

$ pandoc -s -t revealjs -o slide.html input.md 

とりあえず何か作ってみる

次の内容をinput.mdとして作成する。

% スライドのテスト
% Yusuke Mitsuki (発表者)
% 2019/4/1(発表年月日)

# 見出し1

本文本文本文本文本文本文本文本文

## 見出し2

本文本文本文本文本文本文本文本文

### 見出し3

本文本文本文本文本文本文本文本文

作ったものを見る

上記のpandocを実行すると、slide.htmlが作成されている。

これをブラウザで開くとスライドが見れる。

とりあえず、方向キーの左右で進んだり戻ったりできる。

表示効果

マークダウンの構文でいろいろできる。

箇条書きや番号付きリスト、画像の挿入などはそのまま使用できる。

ポーズ

. . .で本文中にポーズを挟むことができる。

まえ

. . .

うしろ

発表者ノート

スライドには表示されない、発表者向けのノートを入れることができる。

<div class="notes">
これはノートです。

- リストのような
- Markdownも含めることができます。
- ノート本文と1行空けて下さい。
</div>

スライド表示中にsを押すと、発表者ノート表示用のポップアップが表示される。 これは現在時刻と開始からの経過時間なども一緒に表示されるので便利。

スライドレベル

--slide-levelオプションを使用すると見出しの階層によってスライドをネストすることができる。

指定した数より上の階層には本文が入れられない(無視される)。例えばスライドレベル2であれば見出し1の下の本文が無視される。

$ pandoc -s -t revealjs -o slide.html input.md --slide-level=2

2以上であれば、縦方向にもスライドするようになる。

個人的には2あれば十分。

テーマの変更

-V theme:テーマで使用するテーマを変更することができる。

テーマ 概要
beige ベージュ色の背景、暗い色の文字、茶色のリンク
black 黒い背景、白い文字、青いリンク (既定のテーマ)
blood 黒い背景、白い文字、赤いリンク
white 白い背景、黒い文字、青いリンク
league 灰色の背景、白い文字、青いリンク (3.0.0以前のreveal.jsの既定のテーマ)
moon 暗い青の背景、ベージュの文字、青いリンク
night 黒い背景、太く白い文字、オレンジ色のリンク
serif カプチーノ色の背景、灰色の文字、茶色のリンク
simple 白い背景、黒い文字、青いリンク
sky 青い背景、細い暗い色の文字、青いリンク
solarized クリーム色の背景、暗い緑の文字、青いリンク

キーバインド

とりあえず次のような事ができる。

キー 機能
f 全画面
o アウトライン表示
n 次のページ
p 前のページ
b 黒画面
s ノートの表示(ポップアップ)
h 左のページ
j 下のページ
k 上のページ
l 右のページ

テーマの追加

テーマを追加したい場合はreveal.js/css/themecssを追加すれば良い。

今回は既存のテーマをベースに新しいテーマmy-theme.cssを作成する。

$ cp reveal.js/css/theme/moon.css reveal.js/css/theme/my-theme.css

次のように変更する。

--- reveal.js/css/theme/moon.css 2019-04-01 13:34:34.249863370 +0900
+++ reveal.js/css/theme/my-theme.css  2019-04-01 15:01:48.082784565 +0900
@@ -20,7 +20,7 @@
 
 .reveal {
   font-family: "Lato", sans-serif;
-  font-size: 40px;
+  font-size: 36px;
   font-weight: normal;
   color: #93a1a1; }
 
@@ -54,18 +54,18 @@
   font-weight: normal;
   line-height: 1.2;
   letter-spacing: normal;
-  text-transform: uppercase;
+  text-transform: none;
   text-shadow: none;
   word-wrap: break-word; }
 
 .reveal h1 {
-  font-size: 3.77em; }
+  font-size: 2.5em; }
 
 .reveal h2 {
-  font-size: 2.11em; }
+  font-size: 1.6em; }
 
 .reveal h3 {
-  font-size: 1.55em; }
+  font-size: 1.3em; }
 
 .reveal h4 {
   font-size: 1em; }

ベースのテーマのフォントサイズが気に入らない場合などはこのようにすると便利。 もちろん全く新規にテーマを作成しても良い。

pandocでは次のようにテーマを指定する。

$ pandoc -V theme:my-theme -s -t revealjs -o slide.html input.md --slide-level=2

pdf出力及び印刷

ブラウザのアドレスバーにあるスライドのURLに?print-pdfを追加する。

その後、ブラウザの印刷機能で印刷する。

PDF出力はブラウザがpdf出力できる場合はそれで行なう。

まとめ

pandocとreveal.jsを使用すると、スライドの作成が簡単になる。

おまけ

機能確認用に作ったinput.md

% スライドのテスト
% Yusuke Mitsuki (発表者)
% 2019/4/1(発表年月日)

# 見出し1

本文本文本文本文本文本文本文本文

<div class="notes">
これはノートです。
</div>

## 見出し2

本文本文本文本文本文本文本文本文

<div class="notes">
スライドには表示されません。
- 箇条書き
- もできます。
</div>


### 見出し3

本文本文本文本文本文本文本文本文

<div class="notes">
ノート画面は意外と便利
</div>



# 画面効果

## リスト

- item1
- item2
- item3

## リスト(*)

* item1
* item2
* item3

## 番号付きリスト

1. item1
1. item2
4. item3
4. item4

## ポーズ

まえ

. . .

うしろ

## イメージ


![サンプル画像](https://cdn-ak.f.st-hatena.com/images/fotolife/m/mickey_happygolucky/20190328/20190328115103.png)

## テーブル

 キー       機能
--------- ---------------------------------------
 f         全画面
 o         アウトライン表示
 n         次のページ
 p         前のページ
 b         黒画面


5行程度が限界かも。

参考

GNU Global(gtags)の対応言語を増やしてみる

はじめに

最近、仕事でgoのコードを読む必要が出てきた。

コードを読む時はemacs+gtagsを使用しているが、デフォルトではgoは対応していない。

ここPygmentsexuberant-ctagsを使ってgtagsの対応言語を増やす方法が紹介されてたので試してみる。

作業環境

作業環境はUbuntu 18.04。

Pygmentsのインストール

Pygmentsはpythonのパッケージで、pipでインストールできる。

$ sudo pip install Pygments

exuberant-ctagsのインストール

Pygmentsの効果を最大限に活かすためにはctagsが必要なのでインストールする。

$ sudo apt-get install exuberant-ctags

aptのgtagsをアンインストール

インストールしたpythonパッケージとの連携を有効にするには自分でビルドする必要があるので、 Ubuntu標準のパッケージでGNU Global(gtags)をインストールしている場合はアンインストールする。

$ sudo apt purge global

GNU Global(gtags)をビルド

次のコマンドでソースをダウンロードしてconfigureスクリプトを実行する。

$ wget http://tamacom.com/global/global-6.6.3.tar.gz
$ tar xvf global-6.6.3.tar.gz
$ cd global-6.6.3
$ ./configure

exuberant-ctagsが有効になっている場合は./configureで次の行が表示されている。

checking for exuberant ctags program... using /usr/bin/ctags

Pygmentsが有効になっている場合は./configureで次の行が表示されている。

config.status: creating plugin-factory/Makefile
config.status: creating plugin-factory/pygments_parser.py

次にmakeを実行してビルド、インストールする。

$ make -j $(nproc)
$ sudo make install

設定ファイル

設定ファイルを/etcの下にコピーする。

$ sudo cp /usr/local/share/gtags/gtags.conf /etc/

実行

$ gtags --gtagslabel=pygments

もしくは環境変数に設定する。

$ export GTAGSLABEL=pygments
$ gtags

まとめ

設定ファイルのコピーを忘れるとgtagsコマンド実行時にタグファイルは作成されるが、 ほぼジャンプできないので注意が必要。

go意外にも対応言語が増えるので、普段からGNU Globalを使用している人にとっては便利。

参考

RISC-Vのベアメタル入門(自分用) 割り込み(CLINT)編 その2

はじめに

追記しました(3/28)

題名がもうなんだかわからなくなってきたが、とりあえずCLINT編の続き。

前回、michaeljclark/riscv-probeの割り込みベクタとハンドラの部分を調査した。

実際にこの部分を動かしているサンプルであるprobe.cを実行してみたところ、割り込みの発生要因がIllegal instructionしかなかった。

自分で任意にソフトウェア割り込みを発生させて、それをトラップしてみたいと思ったので実験することにした。

割り込みベクタの処理を読み直す

前回は、trap_vectorの処理をさらっと読みすぎて重要なことを素通りしてしまった。

sxsp/lxspって何?

trap_vector:
    # Save registers.
    addi    sp, sp, -CONTEXT_SIZE
    sxsp    ra, 0
    sxsp    a0, 1
    sxsp    a1, 2
    sxsp    a2, 3
    sxsp    a3, 4
    sxsp    a4, 5
    sxsp    a5, 6
    sxsp    a6, 7
    sxsp    a7, 8
    sxsp    t0, 9
    sxsp    t1, 10
    sxsp    t2, 11
    sxsp    t3, 12
    sxsp    t4, 13
    sxsp    t5, 14
    sxsp    t6, 15

    # Invoke the handler.
    mv      a0, sp
    csrr    a1, mcause
    csrr    a2, mepc
    jal     trap_handler

    # Restore registers.
    lxsp    ra, 0
    lxsp    a0, 1
    lxsp    a1, 2
    lxsp    a2, 3
    lxsp    a3, 4
    lxsp    a4, 5
    lxsp    a5, 6
    lxsp    a6, 7
    lxsp    a7, 8
    lxsp    t0, 9
    lxsp    t1, 10
    lxsp    t2, 11
    lxsp    t3, 12
    lxsp    t4, 13
    lxsp    t5, 14
    lxsp    t6, 15
    addi sp, sp, CONTEXT_SIZE

    # Return
    mret

スタックポインタに書き込む命令なんだと思ったが、Google先生に聞いても何一つ情報が見つからない。

それもそのはず、自前のマクロだった。

riscv-probe/env/common/rv32/macros.sriscv-probe/env/common/rv64/macros.sにそれぞれ定義があった。 v32のmacros.sを示す。

.equ REGBYTES, 4

.macro lx a, b
lw \a, \b
.endm

.macro sx a, b
sw \a, \b
.endm

.macro lxsp a, b
lw \a, ((\b)*REGBYTES)(sp)
.endm

.macro sxsp a, b
sw \a, ((\b)*REGBYTES)(sp)
.endm

.macro .ptr a
.4byte \a
.endm

次にrv64のmacro.s

.equ REGBYTES, 8

.macro lx a, b
ld \a, \b
.endm

.macro sx a, b
sd \a, \b
.endm

.macro lxsp a, b
ld \a, ((\b)*REGBYTES)(sp)
.endm

.macro sxsp a, b
sd \a, ((\b)*REGBYTES)(sp)
.endm

.macro .ptr a
.8byte \a
.endm

要するに32/64のビット幅を吸収している。

やっている事自体は、スタックにレジスタを退避/復帰しているってことで間違いはない。

自分でソフトウェア割り込みを発生する

トラップの部分はriscv-probeをそのまま使用できるので、割り込みを自分で発生させる方法を調べた。

処理手順としては次のような感じ。

f:id:mickey_happygolucky:20190328115103p:plain

msipのLSBを1に設定すると割り込みが発生する。

この処理を追加するのはどこでも良かったのだが、とりあえずriscv-probe/env/qemu-sifive_e/crt.sに こんな感じで処理を追加した。

    .equ   RISCV_MSTATUS_MIE, 0x08
    .equ   RISCV_MIE_MSIE,    0x08
    
    .text
    .globl soft_intr
    .type soft_intr,@function
soft_intr:
    li    t0, RISCV_MSTATUS_MIE
    csrrs zero, mstatus, t0 /* mstatus.mie = 1 */
    
    li    t0, RISCV_MIE_MSIE
    csrrs zero, mie, t0     /* mie.msie = 1 */
    
    lui t0, 0x02000         /* t0 = 0x02000000(msip for hart0)*/
    li  t1, 0x1             /* t1 = 1 */
    sw  t1, (t0)            /* *(0x02000000) = 1 */
    ret

超ベタ書き。

割り込みハンドラの方で、要因をクリアしないと割り込みが入り続けるので次のようにした。

static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc)
{
    unsigned int mie;

    printf(".");

    mie = read_csr(mie); /* mie読み込み */
    mie &= ~0x8;         /* mie.msieクリア */
    write_csr(mie, mie); /* mie書き込み */
    write_csr(mepc, mepc + 4);
}

csrrcを使えば1発でクリアできるので、少し冗長だが今回は実験なのでこれでよし。

これは間違い。これでは次にmie.msieに1をセットした時点で割り込みが発生してしまう。 ただ割り込みを許可しただけで次の割り込みが発生するのはおかしい。

あと、復帰時点のPCを設定するためのwrite_csr(mepc, mepc + 4);の行も不要。これがあるとおかしくなる。 何もしなくてもちゃんと割り込み時点のpcの次の命令から実行してくれるっぽい。

最終的にはこんな感じ。

static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc)
{
    unsigned int mie;
    volatile unsigned int *msip = (volatile unsigned int *)0x2000000;

    printf(":(0x%08x):\n", *msip);

    *msip = 0;
    write_csr(mcause, 0x0);
    clear_csr(mie, 0x8);
    /*write_csr(mepc, mepc + 4);*/
}

感触としてはmcauseのクリアは不要な気がする。 次の割り込みが入った時点で上書きされるだろうし。

CLINT.msipをクリアしてやる必要があるというの正しい答えだと思われる。

実験に使ったメイン関数。

int main(int argc, char **argv)
{
    char buf[32];
    unsigned int mcause;
    printf("isa: %s\n", isa_string(buf, sizeof(buf)));
    set_trap_fn(trap_save_cause);

    mcause = read_csr(mcause);
    printf("01 mcause(%08x)\n", mcause);

    soft_intr();

    mcause = read_csr(mcause);
    printf("02 mcause(%08x)\n", mcause);
    write_csr(mie, 0x8);
    printf("done\n");

    return 0;
}

実行結果はこうなる。

isa: rv32imacu
01 mcause(00000000)
:(0x00000001):
02 mcause(00000000)
done

write_csr(mie, 0x8)の行で割り込みが発生していない。

まとめ

CLINT完全に理解してなかった。 写経だけでは不十分なこともある。

NuttX ビルトインアプリケーションを作成する

はじめに

NuttXにビルトインアプリケーションを追加する方法を調査した。

ここが日本語でよくまとまっていた。

プログラムを作成する場所

プログラムをどこに追加するかと言うのが一つ考えどころだが、apps/README.txtによると、

Use of the name ''apps/external'' is suggested because that name
is included in the .gitignore file and will save you some nuisance
when working with GIT.

ということなので、apps/external以下にプログラムを追加する。

$ mkdir -p external/helloworld
$ cd external/helloworld

apps/external以下はリポジトリの管理からは外れているので自分で好き勝手に管理することができる。

$ git init

ただし、externalより下のディレクトリはビルド対象に含まれていないので、シンボリックリンクを貼る。

$ ln -s external/helloworld helloworld

プログラムの作成

次のファイル作成する。

  • Kconfig
  • Make.defs
  • Makefile
  • helloworld_main.c

これらは、apps/examples/helloを参考に、というかコピーしてきて自分のプログラムに合わせて変更する感じで良い。

Kconfig

config EXTERNAL_HELLO
    tristate "\"Hello, World!\" example"
    default n
    ---help---
        Enable the \"Hello, World!\" example

if EXTERNAL_HELLO

config EXTERNAL_HELLO_PROGNAME
    string "Program name"
    default "helloworld"
    depends on BUILD_LOADABLE
    ---help---
        This is the name of the program that will be use when the NSH ELF
        program is installed.

config EXTERNAL_HELLO_PRIORITY
    int "Hello task priority"
    default 100

config EXTERNAL_HELLO_STACKSIZE
    int "Hello stack size"
    default 2048

endif

この例では、次のコンフィグレーションがmake menuconfigで設定出来るようになっている。

  • プログラム名
  • タスクの優先度
  • スタックサイズ

今回は無いが、他のライブラリを使用する場合などは依存関係も定義できる。

プログラム名については後で詳しく説明する。

Make.defs

make menuconfigでプログラムが有効化された場合に、CONFIGURED_APPSにプログラムのディレクトリをappsからの相対パスで追加する。

ifneq ($(CONFIG_EXTERNAL_HELLO),)
CONFIGURED_APPS += helloworld
endif

CONFIGURE_APPSに追加されたディレクトリがビルド対象となる。

Makefile

-include $(TOPDIR)/Make.defs

# Hello, World! built-in application info

CONFIG_EXTERNAL_HELLO_PRIORITY ?= SCHED_PRIORITY_DEFAULT
CONFIG_EXTERNAL_HELLO_STACKSIZE ?= 2048

APPNAME = helloworld

PRIORITY  = $(CONFIG_EXTERNAL_HELLO_PRIORITY)
STACKSIZE = $(CONFIG_EXTERNAL_HELLO_STACKSIZE)

# Hello, World! Example

ASRCS =
CSRCS =
MAINSRC = helloworld_main.c

CONFIG_EXTERNAL_HELLO_PROGNAME ?= hello$(EXEEXT)
PROGNAME = $(CONFIG_EXTERNAL_HELLO_PROGNAME)

MODULE = CONFIG_EXTERNAL_HELLO

include $(APPDIR)/Application.mk

ここでは主に次の変数を設定している。

変数 概要
APPNAME アプリケーション名
PRIORITY タスクの優先度
STACKSIZE スタックサイズ
ASRCS アセンブラソース
CSRCS Cソース
MAINSRC メインのソース
PROGNAME プログラム名
MODULE ローダブルバイナリ用

APPNAME、PRIORITY、STACKSIZEの項目はKconfigで設定されてない場合のデフォルト値を考慮した記述になっている。

MODULEはCONFIG_EXTERNAL_HELLOにmを設定した場合に正しくローダブルバイナリを出力(ローダブルビルド)できるようにするために必要な変数。

ローダブルバイナリ

NuttXはelfをロードする機能を持っているので、アプリケーション単体をelfファイルとして作成し、 NuttX上のファイルシステムにおいてある動的にロード、実行することができる。

フラットビルドの場合はローダブルバイナリだったとしても実行時のアドレス空間は単一となる。

ローダブルビルドを有効にするにはCONFIG_BUILD_LOADABLEをyにする必要がある。その上でアプリのコンフィグをmに設定する。

APPNAMEとPROGNAME

APPNAMEとPROGNAMEで何が違うのか。

詳細はAPPNAME vs. PROGNAMEを参照。

ただ、リンク元の情報は古くなっているので今のBUILD_LOADABLEBUILD_KERNELの説明がごちゃまぜになっている。 以前はカーネルビルド(BUILD_KERNEL)でしかローダブルバイナリのロードができなかったようなので、その名残だと思われる。

フラットビルドかつローダブルビルドしない、つまり単一のバイナリに結合されるケースについて特に用語見つからなかったので、 ここでは「単一ビルド」と呼ぶことにする。

単一ビルドの場合、アプリケーションはlibapps.aというライブラリとして生成されnuttxにリンクされる。 多くの場合プログラムのエントリポイントはmain()だが、単一ビルドの場合は、同じシンボルを使用できないのでそれぞれにエントリポイントの名前つける必要がある。

nshでは$(APPNAME)_mainをそれぞれのアプリケーションのエントリポイントとして探すというルールがあるため、単一ビルドの場合はAPPNAMEが必要となる。

PROGNAMEはローダブルビルドした場合に生成される実行ファイルの名前を設定する。

そのため、これらはそれぞれ別の役割を持っていると言える。

APPNAMEとエントリポイントとMAINSRC

先述の通りAPPNAMEは単一ビルド時にアプリケーションのエントリポイントを探すために使用される。 そのためAPPNAMEとエントリポイントとなる関数の名前は次のルールに則っている必要がある。

$(APPNAME)_main

また、MAINSRCに設定するファイル名もエントリポイントと一致している必要がある。

APPNAMEが「helloworld」だった場合の設定例を次に示す。

項目 概要
APPNAME helloworld
エントリポイント helloworld_main
MAINSRC helloworld_main.c

単一ビルド時の

helloworld_main.c

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#include <stdio.h>

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * hello_main
 ****************************************************************************/

#if defined(BUILD_MODULE)
int main(int argc, FAR char *argv[])
#else
int helloworld_main(int argc, char *argv[])
#endif
{
  printf("!!Hello, World!!\n");
  return 0;
}

BUILD_MODULEが定義されている時は、エントリポイントをmain、それ意外ではhelloworld_mainとしている。

BUILD_MODULEはプログラムをローダブルビルドした場合に自動的に定義される。

ビルド

nuttxディレクトリで次のコマンドを実行する。

$ make clean
$ make menuconfig

メニューで追加したプログラムを有効化してビルドする。

単一ビルドの場合は、次のようにBuiltin Apps:にhelloworldが追加される。

nsh> help
help usage:  help [-v] [<cmd>]

  [         cp        exec      kill      mv        set       uname
  ?         cmp       exit      ls        mw        sh        umount
  basename  dirname   false     mb        ps        sleep     unset
  break     dd        free      mkdir     pwd       test      usleep
  cat       df        help      mh        rm        time      xd
  cd        echo      hexdump   mount     rmdir     true

Builtin Apps:
  hello       helloworld

参考

RISC-Vのベアメタル入門(自分用) 割り込み(CLINT)編

はじめに

RISC-VというかFE310の割り込み周りについて勉強する。

取っ掛かりとしてはここが分かりやすかった。

割り込みの種類

RISC-V ISAではグローバル割り込み(global interrupt)ローカル割り込み(local interrupt)が定義されている。

HART(HARdware Thread)とか割り込みソースとか色々言葉が出てきてややこしい。

HARTは複数いる可能性がある。HART毎にローカル割り込みソースがあって、それらはグローバル割り込みを介さない。

グローバル割り込みソースはPLIC(Platform-Level Interrupt Controller)が処理して、これは主にペリフェラルの割り込みを処理する。

HARTはローカル割り込みソースを複数持てるが、FE310ではローカル割り込みソースはCLINT(Coreplex Local INTerrupts)しかいない。

FE310ではCLINTは次の3つしか処理しない。

  1. ソフトウェア割り込み(SI)
  2. タイマー割り込み(TI)
  3. 外部割り込み(EI)

HARTもHART0しか見当たらない。

グローバル割り込みソースであるところのPLICで処理された割り込みはCLINTの外部割り込み(EI)に投げられてくる。

こんな理解。

f:id:mickey_happygolucky:20190315164120p:plain

でも、SiFive FE310-G000 Manual v2p3では「Figure 9.1: E31 Interrupt Architecture Block Diagram.」となっていた。

f:id:mickey_happygolucky:20190315165457p:plain

まぁ、きっとそういうことなんだろう。

割り込みベクタ

割り込みが発生するとMTVEC(Machine Trap VECtor)に登録されたアドレスに処理が飛ぶ。

割り込みベクタにはシングルモードとマルチモードがある。 マルチモードの場合はMTVECに登録されたアドレスはベクターテーブルの先頭アドレスとなり、要因によってオフセットされたアドレスに処理が飛ぶことになる。

参考になる実装を見てみる

@LDScellさんに教えてもらったmichaeljclark/riscv-probeを見てみることにする。

割り込みベクタの初期化

env/qemu-sifive_e/crt.s

.include "crtm.s"

本体はenv/common/crtm.s

MTVECを初期化している部分はここ

#
# start of trap handler
#

.section .text.init,"ax",@progbits
.globl _start

_start:
    # setup default trap vector
    la      t0, trap_vector
    csrw    mtvec, t0

    # set up stack pointer based on hartid
    csrr    t0, mhartid
    slli    t0, t0, STACK_SHIFT
    la      sp, stacks + STACK_SIZE
    add     sp, sp, t0

    # park all harts excpet hart 0
    csrr    a0, mhartid
    bnez    a0, park

    # jump to libfemto_start_main
    j       libfemto_start_main

    # sleeping harts mtvec calls trap_fn upon receiving IPI
park:
    wfi
    j       park

HARTごとにスタック割り当ててるが、FE310ではHART0しか無い。

割り込みベクタ

MTVECにtrap_vectorを設定している。

trap_vector:
    # Save registers.
    addi    sp, sp, -CONTEXT_SIZE
    sxsp    ra, 0
    sxsp    a0, 1
    sxsp    a1, 2
    sxsp    a2, 3
    sxsp    a3, 4
    sxsp    a4, 5
    sxsp    a5, 6
    sxsp    a6, 7
    sxsp    a7, 8
    sxsp    t0, 9
    sxsp    t1, 10
    sxsp    t2, 11
    sxsp    t3, 12
    sxsp    t4, 13
    sxsp    t5, 14
    sxsp    t6, 15

    # Invoke the handler.
    mv      a0, sp
    csrr    a1, mcause
    csrr    a2, mepc
    jal     trap_handler

    # Restore registers.
    lxsp    ra, 0
    lxsp    a0, 1
    lxsp    a1, 2
    lxsp    a2, 3
    lxsp    a3, 4
    lxsp    a4, 5
    lxsp    a5, 6
    lxsp    a6, 7
    lxsp    a7, 8
    lxsp    t0, 9
    lxsp    t1, 10
    lxsp    t2, 11
    lxsp    t3, 12
    lxsp    t4, 13
    lxsp    t5, 14
    lxsp    t6, 15
    addi sp, sp, CONTEXT_SIZE

    # Return
    mret

次のことを行っている。

  1. レジスタの退避
  2. trap_handlerの呼び出し
  3. レジスタの復帰
  4. プログラムへの復帰

trap_handlerの呼び出し

trap_handlerの呼び出しはここ。

    # Invoke the handler.
    mv      a0, sp
    csrr    a1, mcause
    csrr    a2, mepc
    jal     trap_handler

C言語だと次のようなイメージ。

trap_handler(sp, mcause, mepc);

spはスタックポインタ、mepcとmcauseはCSRレジスタ

レジスタ Description
mepc Machine exception program counter.
mcause Machine trap cause.

mcauseは割り込み要因。 mepcは例外時のプログラムカウンタ。

exception? は割り込み発生時のPCという理解で良いのだろうか。

The RISC-V Instruction Set Manualによると、

When a trap is taken into M-mode, mepc is written with the virtual address of the instruction that
encountered the exception.

なるほど。わからん。

SiFive FE310-G000 Manual v2p3の「9.2 Interrupt Entry and Exit」にわかりやすい説明があった。

The current pc is copied into the mepc register, and then pc is set to the value of mtvec. In the
case where vectored interrupts are enabled, pc is set to mtvec.BASE + 4×exception code.

やはりその理解で良さそうだ。

プログラムへの復帰

割り込み発生前のプログラムに復帰するのはmretらしい。

    # Return
    mret

mretを実行すると次のことが行われる。

  • The privilege mode is set to the value encoded in mstatus.MPP.
  • The value of mstatus.MPIE is copied into mstatus.MIE.
  • The pc is set to the value of mepc.

この時点でmepcに設定されているアドレスに復帰するということか。

割り込みハンドラ

trap_handlerの定義を探す。./libfemto/arch/riscv/trap.cにあった。

...(snip)...

static trap_fn tfn = 0;

...(snip)...

trap_fn get_trap_fn()
{
    return tfn;
}

void set_trap_fn(trap_fn fn)
{
    tfn = fn;
}

void trap_handler(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc)
{
    if (tfn) {
        tfn(regs, mcause, mepc);
    } else {
        die("machine mode: unhandlable trap %d @ %p", mcause, mepc);
    }
}

static変数としてユーザーが定義するハンドラのポインタとなるtfnを定義。

set_trap_fn()で設定。

trap_handlerが割り込みベクタから直接呼び出される。trap_handlerではtfnに設定されている関数を呼び出す。

f:id:mickey_happygolucky:20190315164046p:plain

このようになるイメージ。

使用例

割り込みの使用例はexamples/probe/probe.c

...(snip)...

static void probe_all_csrs()
{
    int *csrenum = csr_enum_array();
    const char **csrnames = csr_name_array();
    const char* ws = "               ";
    set_trap_fn(trap_save_cause); //ここでハンドラを設定
    while (*csrenum != csr_none) {
        save_mcause = MCAUSE_UNSET;
        long value = read_csr_enum(*csrenum);
        const char* csrname = csrnames[*csrenum];
        if (save_mcause != MCAUSE_UNSET) {
            int async = save_mcause < 0;
            int cause = save_mcause & (((uintptr_t)-1) >> async);
            printf("csr: %s%s %s cause=%ld mtval=0x%lx\n",
                csrname, ws + strlen(csrname), cause < 16
                ? (async ? riscv_intr_names : riscv_excp_names)[cause]
                : "(unknown)", save_mcause, read_csr_enum(csr_mtval));
        } else {
            printf("csr: %s%s 0x%lx\n",
                csrname, ws + strlen(csrname), value);
        }
        csrenum++;
    }
}

...(snip)...

設定されたハンドラはtrap_save_cause()

static void trap_save_cause(uintptr_t* regs, uintptr_t mcause, uintptr_t mepc)
{
    save_mcause = mcause;
    write_csr(mepc, mepc + 4);
}
#define write_csr(reg, val) ({ \
  asm volatile ("csrw " #reg ", %0" :: "rK"(val)); })

write_csr()の第1引数は#regによって文字列化するのでレジスタ名と解釈される。 write_csr(mepc, mepc + 4);はmepcにmepc+4を書き込むということになる。

割り込み前の位置の1つ次の命令のアドレス(mepc+4)に復帰するようにmepcを書き換えている。

まとめ

CLINT完全に理解した

参考資料