不注意でよく物を壊す

アクアリウムとかマイクロマウスとかアリュージョニストとかそのへん

情弱でもNimでLチカしたい!on Nucleo-F401RE

この記事はstm32 Advent Calendar 2017の7日目の投稿です.間に合ったぜ.

動機

わたしの近況なのですが,最近はマウスのモチベが下がって研究室配属などがあり,マイコンプログラミングから離れてPythonばかり書いています.なので久しぶりにCを書こうとするととても辛い.そこでCの資産が使えて書きやすい言語がないかなーと探していたところ,Nimという言語に行き当たりました.

Nimについて

詳しくは以下の記事などを読んでください.

インストールに関しては
公式から落としてくるか,Githubを参照してください.
簡単にいうとPythonっぽい記法でC並の速度が出るモダンな感じの静的言語です.

下準備

前回の記事にしたがって準備を進めます.
一点,前回とは違い,今回は話を簡単にするために.c/.hを別ファイルにするチェックを外します.
f:id:tattaka5X:20171206205219p:plain
生成形式は前回の記事と同じくmakefile形式です.
これで初期化コードを生成します.

手段

自分は全体的な作業の流れとして,以下のようなやり方を考えました.
Nimの公式から提供されている変換ツール「c2nim」を用いてmain.cに依存するヘッダーファイルをNimファイルに変換してライブラリごとバインドするというやり方です.
このやり方は結果として失敗に終わるので詳細は割愛しますが,まずc2nimのビルドに失敗します.ソースのバージョンを落とし,中身をコメントアウトしてビルド成功,実際に使ってみるとマクロがうまく変換されず無限にエラーが出ます.楽しいですね.
次に考えたのが,cファイルで関数をラップしてnimファイルに渡し,それをcファイルにコンパイルし(Nimは一旦cファイルに変換してからgccコンパイルしてるっぽい)他のファイルとリンクさせてビルドするというやり方です.

実装

基本的にProjectName/Srcの中にファイルを置いていきます.まずはビルドに必要になるnimファイルを作成していきましょう.nimファイルは--os:standaloneオプションをつけてコンパイルするときpanicoverride.nimというファイルが必要になります.

proc printf(frmt: cstring) {.varargs, importc, header: "<stdio.h>", cdecl.}
proc exit(code: int) {.importc, header: "<stdlib.h>", cdecl.}

{.push stack_trace: off, profiler:off.}

proc rawoutput(s: string) =
  printf("%s\n", s)

proc panic(s: string) =
  rawoutput(s)
  exit(1)

{.pop.}

このコードの必要性がよくわからないんですがないとビルドできないので置きましょう.偉い人はなんでこのコードがいるのか僕に教えてください.

STM32CubeMXから吐き出されたコードをいじっていきます.makefileをビルドに用いる都合上,同名のファイルがあった場合一番上の階層のコードをコンパイルする&main関数が複数あるとよろしくないのでmain.cをmain_.cなど別の名前に変えましょう.main_.cの中身ですが,自分はこう書きました.

#include "main.h"
#include "stm32f4xx_hal.h"

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

void main_init(void){
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
}
void LED1_tick(void){
   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}

void SystemClock_Config(void)
{
/*************以下略**************/

簡単に説明すると,上で書いた通り,main関数はnimファイルの中に書くので消去,とりあえずLチカだけしたいので初期設定用の関数をmain_initという名前でまとめ,Nucleoの緑LEDをトグルする関数をLED1_tickという関数でラップしました.それ以外はそのままです.


main.nimの中身はこんな感じで実装します.

{.compile: "./main_.c".}
proc main_init(): void {.importc.}
proc LED1_tick(): void {.importc.}
proc HAL_Delay(ms: uint32){.header: "stm32f4xx_hal.h", importc: "HAL_Delay", varargs.}

when isMainModule:
  main_init()
  while true:
    LED1_tick()
    HAL_Delay(1000)

nimファイルにcの関数を渡す方法は何通りかあって,このファイルではcファイルでラップされた関数をインポートするやり方とヘッダーファイルから関数をインポートするやり方の2通りを実装しています.
前者のやり方ですが,1行目でインポートしたい関数がある.cファイルを指定します.この文法はcompile pragmaと言ってコンパイル時に実行されます.
2,3行目で関数を宣言します.この時,右に.cファイルからインポートされたファイルであるということを示すimport pragmaを追加します.
4行目では,ヘッダーファイルから関数を読み込んでいます.1行目とは違い、nimコマンドでcファイルに変換する時には読み込まれず間違っていても変換されてしまうので注意が必要です.パスですが,makefileに記述されているのに従い指定します.
それ以降はmain関数内に読み込んだ関数を使いLチカのコードを書いています.

ビルド

まず,main.nimをビルドします.projectディレクトリで,

nim cc -c --cpu=arm --d:release --gc:none --os:standalone --deadCodeElim:on src/main.nim

を実行します.
実行するとnimcacheなるディレクトリが作成されます.nimcacheの中のファイル構造は次のようになっていると思われます.
f:id:tattaka5X:20171206223521p:plain
main.cとstdlib_system.cの中身はみてはいけません,目が死にますmain.nimからcファイルに変換された結果が入っています.
main_.sha1はこの場合main_.cが変更された時のみ新しくファイルを生成するためのファイルらしいです.
main.jsonにはcファイルをビルドする手順が書かれていますが今回は無視します.

nimコマンドで生成されたcファイルをビルドする前に,生成されたcファイルで読み込んでいるnimbase.hというファイルをnimcacheの中に置きます.
このファイルは本来ならnim/nim_version/nim/libの中にあってそれを呼び出したいんだろうな〜というファイルなのですが残念ながら呼び出してくれないので同じものを作成します.公式をコピペして持ってくるのが早いです.

最後にmakefileを書き換えます.
前回の記事のようにBINPATHにgcc-arm-none-eabiのパスを入力します.
C_SOURCES= 以下のmain.cをmain_.cに変更し,Src/nimcache/main.cとSrc/nimcache/stdlib_system.cを追加します.
これで

make all

を実行するとめでたくバイナリが生成されます.おめでとうございます.(以下のツイートは多分ソースコードの中身が少し違います)


まとめ

とりあえずNimを使ってLチカすることはできました.しかし,わざわざラップしなくてはいけなかったり,ヘッダファイルから読み込む場合でも引数の型が他のファイルに依存していたりすると面倒なので使い勝手がいいとは言えないです.ただ,Cではマクロを使って無理やり書くしかなかったテンプレートなどメタプログラミングに強い面もあるので(どうせ後でCに変換されるんだけどな)有用な場面も出てくるのではないでしょうか.

おまけ

上に書いたmakefileの仕様で同名のファイルは上の階層のものがビルドされることを知らずに改変前のmain.cをビルドして喜んでいるわたしの様子です.


追記

githubにもあげています.makefileのパスは各自対応お願いします.
github.com