LoginSignup
3
2

More than 5 years have passed since last update.

PureScriptでLチカをしよう(2)

Posted at

前回のあらすじ

PureScriptでLチカをしよう(1)でPureScriptをC++に変換してPC上で実行する事が出来ました.ここから評価ボードへの組込みに向けてコンパイルすることを目標にやっていきましょう.
関数型言語に馴染みのない開発者に向けてPureScriptの単純なコードを示しながら読み進められるように記載を心がけましたが,よくわからない部分やおかしい部分やがあれば,ご連絡いただけるとうれしいです.また,実例によるPureScriptを参考書として合わせて参照してみてください.

市場調査

こんな記事を見ている読者には関係がないかもしれませんが,新しい言語を始めるのは障害が多く難しいものです.特にプロジェクトに取り入れるのはどんな人であっても付随する苦労に尻込みするものです.自分を含めそんな人達の意欲を刺激するため,組込みへのいろいろな言語の導入のトレンドを見てみましょう.

組込み目的で関数型言語を使うというプロジェクトFunctional IoTがあり,有用な情報がまとめられています.

ここの著者は過去にHaskellを組込みやOSに使うためAjhcというプロジェクトを行っており,Functional MCU programming #0: Development environmentにHaskellを組込みで使う上での特徴をまとめています.

LDCでSTM32F103のバイナリを作ろうとしてできなかったではSTM32F103をダーゲットにD言語で組み込みを行っています.トラブルをどのように解決していったのかを細かく記載してあり,クロスコンパイルでの問題解決に有用な情報が掲載されています.

上記以外にも組込み開発に書きやすい言語持ってくるのは,IPythonやmRubyなどをRasbPiでプログラムでしてみたなどの記事は大量にあり十分な要望があるとこであるとわかります.

組込みソフトウェアは個人又は数人で行う小規模なケースが多く,ソフトウェアを納入品としないなど客先から言語の指定がない場合には新しい技術の導入に必要な合意を取る人間の数も少なく済みます.多くの人が一丸となってプロジェクトに取り組む大規模ソフトウェア開発はソフトウェア開発の花形であるようなイメージがありますが,小規模で閉鎖的なプロジェクトも良い部分があります.

こういったことを知っておくと今回のような挑戦的な作業を進める上でモチベーションになります.

はじめに

PureScriptを組込み目的に使うにあたってC++コードを出力させますが,前回触れたようにpurescript-nativeは試験的なプロジェクトでバーションの追従も完璧でなく,また,実行バイナリのサイズを小さくすることは,優先順位がかなり低いと考えられるため,サイズを小さく詰めていくことは難しいと思われます.
上記のFunctional IoTで紹介されているメールではHaskellを用いたOS開発に触れて以下のように

While jhc generates C code, the kind of C code it 
generates may not be suited for kernel.

jhc(Ajhcのフォーク元)はCコードを生成しますが、出力されたコードはカーネルに適していない可能性があると言っています.これはクロスコンパイル全体に言えるでしょう.曖昧な表現ですが,変換前と変換後が大きく異なる場合にはより顕著に起こる現象です.Haskellと文法の似ているPureScriptも同様の問題を持っています.今回は良い感じに変換されることを祈り,ダメな場合には手動で直す方針で行きます.

バイナリサイズに関しても同様に考え,ハードウェアはどんどん進化していること,最終的なコードが小さくなるにしろデバックのために大きなファイルサイズを一時的に動作させる必要があること,IoTは少量多品種なものが多くハードウェアの値段がクリティカルな問題ではないことから,この点には目をつぶり,現実的なファイルサイズ及びメモリサイズであれば良しとしましょう.

評価ボードの選定

組込み対象となる評価ボードについてFunctional IoTの選定を見てみましょう.

選定しているものが少し古いですが,STMicroelectronics社のSTM32シリーズは入手性もよく値段も安いため,STM32F4DISCOVERYで行きましょう.ですが,今回はお金がないため評価ボードの購入を見送ることにしました.変なことにお金をかけている自分のせいですので,今回はシミュレータで我慢して進めることにします.purescript-nativeを使用したクロスコンパイルはC++に一度変換してからバイナリを作成するため,他の評価ボードに関しても今回と同様の手順で進めることができるはずです.

Toolchainの選定

必要な情報についてねむいさんのぶろぐに詳しくまとまっています.
昔はYAGARTOでしたが,現在はGNU Tools for ARM Embedded Processorsをおすすめしているため,コチラを使用していきます.

$ choco install gcc-arm-embedded 

msys2環境から使用するため,ビルドツールのあるフォルダにパスを通しておきましょう.

> arm-none-eabi-gcc --version | head -n1
arm-none-eabi-gcc.exe (GNU Tools for Arm Embedded Processors 7-2017-q4-major)
 7.2.1 20170904 (release) [ARM/embedded-7-branch revision 255204]

> arm-none-eabi-ld --version | head -n1
GNU ld (GNU Tools for Arm Embedded Processors 7-2017-q4-major) 2.29.51.20171128

> arm-none-eabi-gdb --version | head -n1
GNU gdb (GNU Tools for Arm Embedded Processors 7-2017-q4-major) 8.0.50.20171128-git

> arm-none-eabi-as --version | head -n1
GNU assembler (GNU Tools for Arm Embedded Processors 7-2017-q4-major) 2.29.51.20171128

> git --version
git version 2.17.0

今回記載するコードについては,以下に保存しておきます.STMicroelectronics社のコードについては別途保存してください.

https://github.com/noolbar/purescript-native-demo-ltika.git

Lチカコード

上記したようにC言語やC++言語以外でのLチカ実行はいろいろな記事があり,ARM Cortex-M 32ビットマイコンでベアメタル "Safe" Rustなどが参考になります.

全体のプログラムの流れは以下のようになっています.
+ ブート処理
+ ペリフェラルの初期化
+ IOポートのOn/Offを繰り返す

PureScriptは,I/Oポートの状態を示すメモリへ書き込むコードを記載することが出来ません.また,ビット演算を行う演算子も準備されていません.ここでは,関数型言語らしいコードにこだわらず,動作させることを優先してPureScriptから外部に記載したC++コードを呼び出して解決することにしましょう.

ブート処理についてはスタートアップスクリプトを見てみるが参考になります.

今回はSTMicroelectronics社のSTM32CubeF4にあるLL templateをそのまま使えばよいでしょう.

ペリフェラルの初期化及びIOの操作を呼び出しているC++言語で書かれた以下のプログラムをPureScriptに書き換えてうごかすことにしましょう.ポートDの15をHighにして青のLEDを点滅させています.

int main(void)
{
  RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

  GPIOD->MODER = 0x40000000;

  volatile unsigned int count = 0;
  while(1)){
    GPIOD->ODR = 1 << 15;
    for (count = 0; count <= 0x0A037A00;) {count++;}
    GPIOD->ODR = 0;
    for (count = 0; count <= 0x0A037A00;) {count++;}
  }
}

まずは動かしてみる

  1. 作成済みのファイルをGitHubからコピーしてきます.

    > git clone https://github.com/noolbar/purescript-native-demo-ltika.git workDir
    
  2. 上記で紹介したSTM32CubeF4をダウンロードするとen.stm32cubef4.zipが入手できますので,必要なファイルをSTM32Cube_FW_F4_V1.21.0から探して展開します.LL templateで使用している以下のヘッダーファイルをbootfileに保存しましょう.

    STM32Cube_FW_F4_V1.21.0\Drivers\CMSIS\Include
    STM32Cube_FW_F4_V1.21.0\Drivers\CMSIS\Device\ST\STM32F4xx\Include
    STM32Cube_FW_F4_V1.21.0\Drivers\STM32F4xx_HAL_Driver\Inc

    メモリマップも後で使用するのでコピーしておきます.
    STM32Cube_FW_F4_V1.21.0\Projects\STM32F4-Discovery\Templates_LL\SW4STM32\STM32F4-Discovery\STM32F407VGTx_FLASH.ld

    STM32Cube_FW_F4_V1.21.0\Projects\STM32F4-Discovery\Templates_LL\Src\及びSTM32Cube_FW_F4_V1.21.0\Projects\STM32F4-Discovery\Templates_LL\Inc\にあるファイル並びにSTM32Cube_FW_F4_V1.21.0\Projects\STM32F4-Discovery\Templates_LL\SW4STM32\startup_stm32f407xx.Sbootfileに展開します.

    bootfileの構成に以下ファイルが追加されます.(main.cは不要です)

    workDir/
    |-bootfile/
    |---startup_stm32f407xx.s
    |---main.h
    |---stm32_assert.h
    |---stm32f4xx_it.h
    |---stm32f4xx_it.c
    |---system_stm32f4xx.c
    |---STM32F407VGTx_FLASH.ld
    |---STM32Cube_FW_F4_V1.21.0/
    
  3. C++コードを生成します.purescript-nativeの作成は前回の記事を参考に行い,パスを通しておきましょう.

    > cd workDir
    > make clean
    > make codegen
    

    エラーなくコードの生成が出来たでしょうか.
    以降ではgit cloneで持ってきたコードの内容の解説を行っていきながら,Lチカを目指します.とりあえず実行したい場合は次章を飛ばしても問題ありません.

外部コードの呼び出し

FFIと呼ばれる外部関数インタフェース機能をつかって,PureScriptは他の言語で記載されたライブラリをコード内で使うことができます.

  1. 実装となるbootfile/MPU.ccを作成します.これは,C++言語で記載します.外部関数の定義については,purescript-nativeが生成する外部関数を定義しているファイルが役に立ちます.例えば.psc-package\master\console\master\src\Control\Monad\Eff\Console.pursなどを参考にしましょう.

    まずは,ピンをGPIOとして動作させる設定を行うexternfeSetPeripheralという関数を定義します.以下のコードを実行させることで実現させます.

    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    

    設定値は引数として設定したかったので,以下のように作成しました.

    bootfile/MPU.cc
    #include "stm32f4xx_it.h"
    #include "PureScript/PureScript.hh"
    #include "Main/Main_ffi.hh"
    #include "Data.Unit/Unit.hh"
    
    namespace MPUFFI {
      using namespace PureScript;
    
      auto externfeSetPeripheral (const any &flag) -> any {
        return [=]() -> any {
          RCC -> AHB1ENR |= static_cast<const unsigned int>(flag);
          return Data_Unit::unit;
        };
      };
    }
    

    bootfile/MPU.hhでインクルードします.

    bootfile/MPU.hh
    #ifndef MPUFFI_HH
    #define MPUFFI_HH
    
    #include "PureScript/PureScript.hh"
    
    namespace MPUFFI {
      using namespace PureScript;
    
      auto externfeSetPeripheral (const any &flag) -> any;
    }
    #endif
    
    bootfile/Main_ffi.hh
    #include "MPU.hh"
    using namespace MPUFFI;
    

    ペリフェラルの初期化まで出来るようにsrc/Main.pursを記載してみます.

    src/Main.purs
    module Main where
    import Prelude
    import Control.Monad.Eff ( Eff )
    
    foreign import data MEMORY :: !
    foreign import externfeSetPeripheral :: forall eff. Int -> Eff( memory :: MEMORY | eff ) Unit
    
    -- #define RCC_AHB1ENR_GPIODEN 0x00000008
    main :: forall eff a.  Eff( memory :: MEMORY | eff ) a
    main = do
      externfeSetPeripheral 0x00000008
    

    関数の定義にある.Eff( memory :: MEMORY | eff ) Unitの部分が見慣れないコードかと思います.これは,副作用の表現を行っています. 関数の型は「副作用のある計算で、メモリ操作とそれ以外の任意の種類の副作用を備えた任意の環境で実行することができ、型 Unitの値を返す」を表しています.この作用の影響はハンドラを使用して除去するまで伝搬していきます.

  2. 同様にしてピンの出力を制御するexternHAL_GPIO_WritePinや次のステップを遅らせるexternHAL_DelayについてもPureScriptのコードに持ち込めそうです.
    他の部分で問題になりそうな部分として無限ループがあります.実行を続けるために必要な部分ですが,PureScriptにwhileforなどの予約後はありません.これをはどの様にコードを書けばればよいのでしょうか.今回は,無限ループの部分を関数にして自身を呼び出し再帰関数として記述します.

    src/Main.purs
    -- #define RCC_AHB1ENR_GPIODEN 0x00000008
    module main where
    import Prelude
    
    main = do
      unsafeSetPeripheral 0x00000008
      infLoop
      where
        infLoop = do
          infLoop
    
  3. 他の関数を作成して呼び出しを加えた結果は以下のようになりました.手続き的に記載しているので抵抗感は少ないのではないのでしょうか.ペリフェラルの操作に必要なメモリアドレスは直接記載しています.

    src/Main.purs
    module Main where
    import Prelude
    
    import Control.Monad.Eff ( Eff )
    foreign import data MEMORY :: !
    
    foreign import externSystemClock_Config :: forall eff. Eff ( memory :: MEMORY | eff ) Unit
    foreign import externfeSetPeripheral :: forall eff. Int -> Eff( memory :: MEMORY | eff ) Unit
    foreign import externHAL_GPIO_WritePin :: forall eff. Int -> Int -> Int -> Eff( memory :: MEMORY | eff ) Unit
    foreign import externHAL_Delay :: forall eff. Int -> Eff( memory :: MEMORY | eff ) Unit
    
    -- #define RCC_AHB1ENR_GPIODEN 0x00000008
    -- #define PERIPH_BASE         0x40000000U
    -- #define AHB1PERIPH_BASE     (PERIPH_BASE + 0x00020000U)
    -- #define GPIOD_BASE          (AHB1PERIPH_BASE + 0x0C00U)
    -- #define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
    -- #define GPIO_PIN_15         ((uint16_t)0x8000)
    main :: forall eff a.  Eff( memory :: MEMORY | eff ) a
    main = do
      externSystemClock_Config
      externfeSetPeripheral 0x00000008
      infLoop
      where
        infLoop = do
          externHAL_GPIO_WritePin 0x40000000 0x8000 1
          externHAL_Delay 0x05037A00
          externHAL_GPIO_WritePin 0x40000000 0x8000 0
          externHAL_Delay 0x0A037A00
          infLoop
    
  4. 上記ファイルを追加するようにMakefileを編集します.

    BOOTFILEiNCLUDE := -I bootfile -I bootfile/STM32Cube_FW_F4_V1.21.0/Drivers/CMSIS/Device/ST/STM32F4xx/Include -I bootfile/STM32Cube_FW_F4_V1.21.0/Drivers/CMSIS/Include -I bootfile/STM32Cube_FW_F4_V1.21.0/Drivers/STM32F4xx_HAL_Driver/Inc
    BOOTFILESRCS :=  $(call rwildcard,bootfile/,*.c) 
    OBJS = $(BOOTFILESRCS:.c=.o) bootfile/startup_stm32f407xx.o
    DEPS = $(SRCS:.cc=.d) $(BOOTFILESRCS:.c=.d)
    
    %.o: %.cc
      @echo "Creating" $@
      @$(CXX) $(CXXFLAGS) $(INCLUDES) $(BOOTFILEINCLUDE) -MMD -MP -c $< -o $@
    
    %.o: %.c
      @echo "Creating" $@
      @$(CXX) $(CFLAGS) $(BOOTFILEINCLUDE) -MMD -MP -c $< -o $@
    
    %.o: %.s
      @echo "Creating" $@
      @$(CXX) -c $< -o $@
    

実行ファイルを作ってみよう

  1. クロスコンパイラを使って生成したコードから実行ファイルを作成してみましょう.実行時に組込み先を指定しないとエラーを起こしますので,これを指定します.

    > make release SHELL='sh -x' GC=NO CXX=arm-none-eabi-gcc CXXFLAGS=-DSTM32F407xx CFLAGS=-DSTM32F407xx
    
  2. 無事コンパイルできて,リンカが実行されると__sync_synchronizeがないと言われるため,sync_synchronize.cというファイルを作成して,いろいろ参考にしてごまかすコードを記載します.git repositoryからcloneした場合には,すでにファイルがあります.

    bootfile/sync_synchronize.c
    inline void __sync_synchronize()
    {
        asm volatile("" ::: "memory");
    }
    
  3. いろいろなメモリマップわからないと言われてしまいます.Makefileに以下を追加します.

    override LDFLAGS  += -Tbootfile/STM32F407VGTx_FLASH.ld `override LDFLAGS  += -lstdc++ -static`
    

    エラーが発生せずに/output/bin/mainが作成されていれば成功です.

    まだエラーが続く場合の解決のTipsを記載しておきます.実装する必要のある関数が抜けているとき,nmコマンドでシンボルテーブルを確認しながら必要なファイルを加えていきます.

    > arm-none-eabi-nm --demangle --numeric-sort output/bin/main 
    

    エラーが出力されないが,動作しない場合にはgccのライブラリを検索するパスの一覧を-lで確認しましょう.確認してみると変なファイルが読み込まれている場合がありますので,その場合は修正しておきます.

    > arm-none-eabi-gcc --print-search-dirs
    
  4. 生成できた実行ファイルを見てみましょう

    > arm-none-eabi-size output/bin/main
      text    data     bss     dec     hex filename
    296160    2784    7600  306544   4ad70 output/bin/main
    

    この実行ファイルのサイズであれば,書き込みを行うことができそうです.

Lチカまでのながれ

PureScriptのコードを実行ファイルまでビルドすることが出来ました.これを書き込んで動作するか見てみたいと思います.上記したように評価ボードではなく,今回はシミュレータで我慢します.今後の開発を考えると,シミュレータでの動作確認は有用ですので良しとしましょう.

  1. qemuを使うのがデファクトスタンダードでしょう.https://tnishinaga.hatenablog.com/entry/2016/12/31/130000 でLチカをシミュレータ環境で実行する記事があるのでやってみましょう.http://blog.boochow.com/article/456638901.html も参考になります.

    > qemu-system-gnuarmeclipse.exe --version
    GNU ARM Eclipse 64-bits QEMU emulator version 2.8.0 (v2.8.0-646-g2c99a25-dirty)
    Copyright (c) 2003-2016 Fabrice Bellard and the QEMU Project developers
    
  2. qemuでシミュレータ環境を確認しましょう.STM32F4-Discoveryがリストにあるのを確認します.

    > qemu-system-gnuarmeclipse.exe -board help
    
    Supported boards:
      Maple                LeafLab Arduino-style STM32 microcontroller board (r5)
      NUCLEO-F103RB        ST Nucleo Development Board for STM32 F1 series
      NUCLEO-F411RE        ST Nucleo Development Board for STM32 F4 series
      NetduinoGo           Netduino GoBus Development Board with STM32F4
      NetduinoPlus2        Netduino Development Board with STM32F4
      OLIMEXINO-STM32      Olimex Maple (Arduino-like) Development Board
      STM32-E407           Olimex Development Board for STM32F407ZGT6
      STM32-H103           Olimex Header Board for STM32F103RBT6
      STM32-P103           Olimex Prototype Board for STM32F103RBT6
      STM32-P107           Olimex Prototype Board for STM32F107VCT6
      STM32F0-Discovery    ST Discovery kit for STM32F051 line
      STM32F4-Discovery    ST Discovery kit for STM32F407/417 lines
      STM32F429I-Discovery ST Discovery kit for STM32F429/439 lines
      generic              Generic Cortex-M board; use -mcu to define the device
    
    
  3. qemuで作成したプログラムを実行します.

    > qemu-system-gnuarmeclipse.exe --verbose --board STM32F4-Discovery --gdb tcp::3333 --semihosting-config enable=on,target=native --image ./output/bin/main
    

    コンデンサ横のLEDが点灯しているのが確認できれば成功です.

    Ltika

    別なコンソールでgdbを起動すれば,デバッカを使った実行を追う作業も出来ます.

    > arm-none-eabi-gdb -q ./output/bin/main
    

    コマンドでプログラムを実行します.

    (gdb) target remote :3333
    (gdb) load ./output/bin/main
    (gdb) continue
    (gdb) monitor stop
    (gdb) quit
    

実行ファイルをターゲットCPUへの書き込み

今回はチャレンジできませんでしたが,機会があればねむいさんのぶろぐで紹介されているOpenOCDを使ってターゲットボードへの書き込みに挑戦してみます.

この記事の内容は1円もかからないので,実際にLチカさせてみてください.自分の手元で物が動いている様子を見ると感動するものがあります.

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2