見出し画像

SimulinkでTeensyを開発する【Githubにて公開】

ArduinoをSimulinkで開発していると,Arduinoよりも性能が良いTeensyもSimulinkで開発したいなと感じるのですが,あいにくMathworksはいまだ公式にTeensyを対応されていません.おそらくいつかは対応されると思うのですが,自分の研究の上でSimulinkでTeensy開発が必要になったので,上手く動作する方法を探ってみました.

今回作ったプロジェクト

結果的にはある程度はSimulinkでTeensyを動作させることができたので以下のGithubにまとめました.手っ取り早くプログラムを確認したい方はGithubをご覧ください.このnoteでは作るまでのプロセスを綴っていきます.

方針

Arduinoへのビルドプロセス

SimulinkはArduinoに公式対応しており,そのビルドプロセスは大まかに上図のようになっています.Simulinkのtoolboxの一つであるEmbedded Coderを使用することでSimulinkのモデルをArduinoコードであるC/C++に変換してくれます.その後,生成したコードをArduino向けにコンパイルすることで,SimulinkモデルをArduinoへアップロードできます.ただ,TeensyでもArduinoコードが動くので,このSimulinkから出てきたC/C++コードをTeensy向けに少し修正すれば動きそうです.

考案したTeensyへのビルドプロセス

そこで,Teensyへのビルドプロセスで上図のものを考案しました.SimulinkモデルはArduinoをtargetに設定しながらデザインし,Embedded CoderからArduino向けのC/C++を生成します.その後,Teensyにビルドするために生成したコードを修正し,組み込み向けのIDEとして知られるPlatformIOを利用してTeensyへビルドを実現します.そして,これらの手順を,MatlabファイルとしてBuild/Upload.mファイルにまとめることで,ファイル実行のワンクリックでSimulinkモデルをTeensyにビルドします.ちなみに,ここにおけるBuild.mは生成コードのコンパイルまでのプログラムであり,Upload.mはTeensyへのアップロードまでのプログラムになっています.さて,方針も立ったので作ってみましょう.

作ったもの

ファイル・ディレクトリ構成

作ったプロジェクトの中身

上図に今回作ったプロジェクトを示しています.基本ベースはPlatformIOによるプロジェクトを採用し,その中にSimulinkのモデルが入っています.以下ファイル・ディレクトリの役割です.

・src: Teensy向けに修正したC/C++コードを含むディレクトリ
・materials: C/C++コードを修正するためのマテリアルを含むディレクトリ
・Build.m: Simulinkモデルからコード生成・コンパイルまで行うmファイル
・Upload.m: Simulinkモデルからコード生成・コンパイル・アップロードまで行うmファイル
・platformio.ini: PlatformIOの設定ファイル.ここでビルドするターゲットを設定することができる.

使い方

1. Matlabワークスペースをプロジェクトのルートまで移動する.
2. Simulinkモデルをデザインする.(私はよくMatlab function使う)
3. Build.m,またはUpload.mでSimulinkモデルをビルドする.

デモ

今回作ったデモ動画を上に載せています.Teensyのうち何も設定していないピンをオシロスコープに接続しています.SimulinkモデルにPWMを出力するブロックを配置し,その後にUpload.mを走らせるとPWM波形が現れてることからSimulinkモデルがTeensyに反映されていることがわかります.

Main関数でのポイント

#include <Arduino.h>
#include <model.h>

unsigned long preTime = 0;
unsigned long nowTime = 0;
unsigned long interval = 100000; //Step size setted in Simulink

void initLib(void);

void setup() {
  // put your setup code here, to run once:
  initLib(); //initialize fcn 
  preTime = micros();
}

void loop() {
  // put your main code here, to run repeatedly:
  nowTime = micros();
  if (nowTime - preTime > interval){
    model_step();  //step fcn generated by simulink
    preTime = nowTime;
  }
}
//Initialize fcn generated by m-file
void initLib(void){
model_initialize(); //initialize fcn created by simulink
MW_SPIwriteReadSetup(); //For SPI
}

Simulinkを使ってモデルのコード生成を行なっていますが,main関数は自作しています.少し正確に言うとBuild/Upload.mによる生成によってmain関数を用意しています.ここのプログラムで押さえておきたい関数は以下のとおりです.

model_initilize(); //initilize fcn created by simulink
model_step; //step fcn created by simulink

model_initialize関数は,Simulinkモデルに関するinitialize関数となっており,Arduinoでのsetup関数に相当します.model_step関数は,Simulinkモデルがマイステップ計算を行う関数であり,Arduinoでのloop関数に相当します.また,model_stepのステップサイズはinterval変数になっています.initLib関数については後述する(Teensyコードへの修正/動的なinitialize関数, initLibの作成)に記載しています.

Build/Upload.mでのポイント

Simulinkによるコード生成

global dirSrc dirModel modelName spiClock spiBitOrder spiMode

%Set parameter by user
modelName = 'model'; %name of simulink model
spiClock = 2000000; %SPI clock speed
spiBitOrder = 1; %SPI BitOrder
spiMode = 0x00; %SPI MODE

%Set 'src/' directory of PlatformIO
dirSrc = 'src/';

%Set embedded coder
cs = getActiveConfigSet(modelName);
switchTarget(cs,'ert.tlc',[]);

%Generate C-code
slbuild(modelName);

%zip C-code of model with arduino library
load(append(modelName, '_ert_rtw/buildInfo.mat'));
packNGo(buildInfo, 'fileName', modelName);

%Unzip model
dirModel = append(modelName, 'Code/');

if(exist(dirModel))
    rmdir(dirModel, 's');
end

unzip(append(modelName, ".zip"), dirModel);

Build/Upload.mにおけるコード生成までのプログラムを上に示しています.Simulinkモデルのコードを生成するために,Embedded Coderを用いてslbuild関数を使用しています.そうすることで,Simulinkモデルが生成されるのだが,このままではMatlabのArduinoLibraryのコードが含まれていないため,slbuild関数によって生成されたbuildInfo.matを用いてArduinoLibraryのコードこみでzipファイルを作るpackNGo関数を使用し,のちに展開します.ちなみに,ArduinoLibraryのC/C++コードはSimulinkモデルが使用するライブラリに応じて変動しています.

Teensyコードへの修正

Simulinkによって生成されるコードは基本的には#include <Arduion.h>に帰着するのですが,中にはArduinoの深い位置でコーディングしているのもあり,Teensyで動作させるにはそのようなコードを修正する必要があります.修正した項目はPWM, SPI, 不要なコードの排除があります.

PWM

Simulinkから生成されるPWMに関連するコードは以下のものがあります.

・MW_PWM.cpp
・MW_PWM.h
・MW_PWMDriver.c
・MW_PWMDriver.h

その中でも修正したのはMW_PWMDriver.cです.ArduinoではPWM波を出力するのに,デューティ比を引数とするanalogWrite関数を使用しますが,Simulinkがマイナスのデューティ比にも対応しようとしているため,MW_PWMDriver.cでは多様なArduinoボードに向け条件付けされています.しかし,Teensyをビルドする際にその条件に引っかからずPWM波を出力できない問題がありました.これを解決するために,Build/Upload.mにMW_PWMDriver.cの末行にTeensy向けのPWM関数を追記するようにしています.

    %For PWM
    lines = readlines("materials/pwmInfo.txt");
    system('chmod 766 src/MW_PWMDriver.c');
    writelines(lines, 'src/MW_PWMDriver.c', WriteMode='append');

上が実際にPWMの修正を行なっているコードである.コード内のmaterials/pwmInfo.txtにはTeensy用のPWM関数が含まれています.

SPI

Simulinkから生成されるSPIに関連するコードは以下のものがあります.

・MW_SPIwriteRead.cpp
・MW_SPIwriteRead.h
・MW_SPI.h

ここで修正するコードはMW_SPIwriteRead.cppです.このコードでは通常のArduinoでも利用するSPI.hを用いてSPI関数を構成しているのですが,その際のクロック周波数,ビットオーダー,モードの設定が未定義のままになっています.そのため,Build/Upload.mにはMW_SPIwriteRead.cppの冒頭にクロック周波数,ビットオーダー,モードを定義する文字列を挿入してもらっています.これらのパラメータはbuild/Upload.mにて設定することができます.

    %Set SPI 
    if(exist('src/MW_SPIwriteRead.cpp'))
        system('chmod 766 src/MW_SPIwriteRead.cpp');
        system(append(append('echo -e "int _RTT_SPI_CLOCK_ = ', string(spiClock)), ';" > tmp.cpp'));
        system(append(append('echo -e "int _RTT_SPI_BITORDER_ = ', string(spiBitOrder)), ';" >> tmp.cpp'));
        system(append(append('echo -e "int _RTT_SPI_MODE_ = ', string(spiMode)), ';" >> tmp.cpp'));
        system(append('cat ', append(dirSrc, 'MW_SPIwriteRead.cpp >> tmp.cpp')));
        system(append('mv -f tmp.cpp ', append(dirSrc, 'MW_SPIwriteRead.cpp')));
    end

また後述しますが,Arduinoハードウェアinit関数であるMW_ArduinoHWInit.cppを排除したことにより,SPI.begin関数が宣言できない問題も生じた.そのため,Build/Upload.mにinitialize関数を作ってもらうことが対処しました.これについては次に記述します.

不要なコードの排除

Simulinkに生成するコードの中には,たまにArduinoの深いところでコーディングしているものがあります.そのようなコードはアーキテクチャ依存になっているのでTeensyではビルドできません.今回SimulinkはArduino DUEをターゲットにしており,ARM系である以下のようなコードがTeensyでは対応できませんでした.

・arduinoARMScheduler.cpp
・MW_ArduinoHWInit.cpp
・arm_m3_cortex_handler.c
・m3m4m4f_multitasking.c

ただこれらのコードは大まかに確認するとプログラムの最適化あたりに関わっているような印象がします.今回はとりあえずSimulinkモデルがTeensyで動くだけでも良いので,これらのコードは排除します.しかし,MW_ArduinoHWInit.cppを排除したことで,一部必要なinitialize関数が実行できませんでした.

動的なinitialize関数, initLibの作成

そこで,Build/Upload.mにはmodel_initialize関数を含めた新たなinitialize関数である,initLib関数を作ってもらうことにしました.とは言っても,今の所追加する項目はmodel_initialize関数とSPI.begin関数暗いです.

function generateInitFcn()
    %Set direcotries 
    global dirSrc dirModel modelName

    %Write code
    writelines('//Initialize fcn generated by m-file', 'src/main.cpp', WriteMode='append');
    writelines('void initLib(void){', 'src/main.cpp', WriteMode='append');
    lines = append(modelName, '_initialize(); //initialize fcn created by simulink');
    writelines(lines, 'src/main.cpp', WriteMode='append');

    %Write SPI init code
    if(exist('src/MW_SPIwriteRead.cpp'))
        lines = 'MW_SPIwriteReadSetup(); //For SPI';
        writelines(lines, 'src/main.cpp', WriteMode='append');
    end

    lines = "}";
    writelines(lines, 'src/main.cpp', WriteMode='append');
end

上はBuild/Upload.m内で自作したgenerateInitFcn関数の中身です.model_initialize関数は無条件で挿入しています.また,'src'ディレクトリにMW_SPIwriteRead.cppが含まれていたら,SPI通信を行うと判断してSPI.begin関数を挿入するようにしています.そして作成したinitLib関数は'src'に含まれるmain.cppの末尾に追加するようにします.今はまだSPIくらいしか考慮がありませんが,これから開発する上で何かinitialize関数をしていないことが起きると思うのでその時はその都度ここに挿入したいと思います.

終わりに

以上,TeensyをSimulinkで開発できるようにするSimulinkWithTeensyの説明でした.あんまりダラダラ書くのは良くないので数時間で急いで書いたので分かりにくいところもあると思うので,空いた時間にでも誤字・脱字等を修正しておきます.では,楽しいTeensyライフを.

【プロフィール/SNS】
Tonaliです.
普段,芝生による動画表示の実現を目指した研究活動を行なっています.

私のポートフォリオサイトはこちらです.
https://tonali-kojiro.com

日々のつぶやきのTwitterはこちらです.
https://twitter.com/maison159777

いつ死んでもいいように遺影を取り続けるInstagramはこちらです.
https://www.instagram.com/tonali_insta/

この記事が気に入ったらサポートをしてみませんか?