鉄火巻キのメモ書き

ただのメモ代わり

Windows + VSCodeでCUDAの環境構築

結論から言って現状(2022/9)ほぼ不可能です. 一応ビルドまでは通せないこともないので順を追って説明します. 回避策としてVisual Stuidioでの環境構築も説明します.

ほかにもWSL2を使うなどしてWindowsでCUDAの開発を行う方法はあります.気が向いたら書くかも.

(追記)WSLもダメっぽい...?

forums.developer.nvidia.com

素のWindows環境

公式やいろんなところが説明しているようにVisual Studioで開発するのが手っ取り早い().

最初に述べたように完全な環境をVSCodeで整えるのは無理ですが,できるとこまで説明します.ここでいう完全な環境とはビルドからデバッグまでできることを指します.別にVSCodeでもコード補完やビルド,実行まではできます.ただ デバッグができません . これは公式にそう書いてあります.正確に言うとwindows用のデバッガは存在するけれどもVSCode拡張機能が対応していません.

それでも解決方法があるという方は教えてください...

VSCodeで環境構築する

ツール類の導入

VSCodeC/C++の環境ができている前提で進めます. まずは拡張機能として Nsight Visual Studio Code Edition を入れます. これはCUDAのコード補完やなんやかんやをしてくれます.ここでデバッガの設定もできるのですが Linux専用機能 です.

そんでもってCUDA Toolkitをインストールします.私はTensorflowもpytorchも使う予定がなかったので最新verを入れました.そこらへんは用途に合わせて入れてください.ただし,Visual Studioでの開発予定があるのであればVisual Studioを先にインストールしてください.そうしないとVisual Studioで開発するためのプラグインがインストールされません.Visual Studioでの開発予定がなくとも後述の通りVisual Studioが必要となるので入れる順番は合わせた方がよいでしょう.

CUDAを入れたらコンパイラにパスを通します.おそらくインストール時にデフォでパスが通っているでしょうが確認しておいた方がよいでしょう.CUDAのコンパイラC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\[バージョン]\bin にある nvcc.exe です.まだ通すべきパスがあるのでここで再起動はしなくていいです.

最後に必要となるのは Visual Studio です.わざわざVisual Studioでの開発を回避してまでVSCodeにしたのに何故...?となりますが,理由はmsvcコンパイラが欲しいからです.WindowsでのCUDAがサポートしているC/C++コンパイラここに書いてあります. 例えばCUDA Toolkit v11.7.1であればサポートは以下です.

CUDAがサポートするコンパイラ
見事にmsvcコンパイラしかありません.無論サポート対象外のコンパイラコンパイルしてもできないことは無いですがおとなしく対象のを使うのが無難でしょう.( -ccbin, -allow-unsupported-compiler 等のオプションでググるとよい)
msvcコンパイラをインストールしたらこちらにもパスを通します.こっちのパスは勝手に設定されることはないはずなのでちゃんと設定します.msvcは C:\Program Files\Microsoft Visual Studio\[西暦]\Community\VC\Tools\MSVC\[バージョン]\bin\Hostx64\x64 にある cl.exe です.

これでPCを再起動してnvcc, clのコマンドが通れば環境の構築は終了です.

プロジェクトの設定

環境の構築が終わったらプロジェクトを用意して実際に動かしてみます.

適当にmain.cuとかいうファイルを生成してこれをビルドします.

まずはc_cpp_properties.jsonの設定です.ここはとりあえず"compilerPath"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/[バージョン]/bin/nvcc.exeにします.あとはお好みでいいでしょう.

 {
    "configurations": [
        {
            "name": "Win32",
            "includePath": [
                "${workspaceFolder}/**",
                "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v11.7/include"
            ],
            "defines": [
                "_DEBUG",
                "UNICODE",
                "_UNICODE"
            ],
            "compilerPath": "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v11.7/bin/nvcc.exe",
            "cStandard": "c17",
            "cppStandard": "c++20",
            "intelliSenseMode": "windows-msvc-x64"
        }
    ],
    "version": 4
}

次にtask.jsonです.自分は以下のようにビルドだけのタスクとビルド&実行のタスクを用意しています.

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "C/C++: nvcc.exe アクティブなファイルのビルド",
            "type": "shell",
            "command": "nvcc",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${workspaceFolderBasename}.exe"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
     },
        {
            "label": "C/C++: nvcc.exe アクティブなファイルのビルドと実行",
            "type": "shell",
            "command": "${fileDirname}/${workspaceFolderBasename}.exe",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "dependsOn": [
                "C/C++: nvcc.exe アクティブなファイルのビルド"
            ]
        }
    ]
}

そしてlaunch.jsonなのですが...要りません.最初に入れた拡張機能のおかげで"CUDA C++: Launch"という設定が可能なのですが,これの"type"である"cuda-gdb"は最初に述べたようにLinux環境でしか動きません.
試しに公式にのっとって以下のようにファイルに記述して実行してみてください.「Linuxでしか使えません」と怒られます.

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "CUDA C++: Launch",
            "type": "cuda-gdb",
            "request": "launch",
            "program": "${fileDirname}/${workspaceFolderBasename}.exe",
            "preLaunchTask": "C/C++: nvcc.exe アクティブなファイルのビルド"
        }
    ]
}

というわけでlaunch,jsonはなしでプロジェクトの設定は完了です.あとはビルドが通るようなコードを書いてCTRL+SHIFT+Bを押してtask.jsonに記述したビルドまたはビルドと実行をして無事ビルドが通ればおしまいです.

以下はどうしてもWindowsでデバッガまで回したいという人向けにVisual Studioでの構築を説明します.

Visual Studioで環境構築する

公式的にwindowsでサポートされているやり方.

先述の通りCUDA Toolkitよりも先に対応のVisual Sutudioがインストールされている必要があります.もし順番を間違えてしまったらそのままCUDA Toolkitを再インストールしてPCを再起動しましょう.

CUDA Toolkitインストール後最初のVisual Studio起動時に以下のポップアップが出るのでEnableします.

そうしたら適当にテストプロジェクトを作ります.C/C++だったら大体なんでもいいと思います. ここではWindowsコンソールアプリケーションで説明します.

まずビルドのカスタマイズを行います.プロジェクト->ビルドのカスタマイズを選択し,CUDAにチェックを入れます.

もし一覧にCUDAが無いようであれば C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA[バージョン]\extras\visual_studio_integration\MSBuildExtensions 下のファイルを C:\Program Files\Microsoft Visual Studio[西暦]\Community\MSBuild\Microsoft\VC[バージョン]\BuildCustomizations にすべてコピーして一覧の更新を行います. そして新規ファイルの追加でCUDAソースファイルを追加します.これはまっさらなファイルで生成されます.ここでプロジェクトプロパティに CUDA C/C++ の項目が追加されていればOKです.無いようであればVisual Studioの再起動などを試してみてください.

次にプロジェクトの設定をしていきます.
プロジェクトのプロパティを開き, C/C++->全般->追加のインクルードディレクトCUDA C/C++->Common->Additional Include Directries$(CUDA_INC_PATH) を追加. リンカー->全般->追加のライブラリディレクト$(CUDA_PATH)/lib/$(PlatformName) 追加.リンカー->入力->追加の依存ファイルcudart.lib を追加します.
これで適当な.cuファイルにmain関数をぶち込んでビルド&デバッグができれば設定は終わりです.

ただこのままだとCUDAのライブラリ関数や構文にエラーが付きます.ビルドは問題ないので実用に支障はありませんが,どうしても気になるという人は明示的に #include<cuda_runtime.h> など適宜ヘッダをインクルードすればエラーを消せます.(<<<のsyntax errorは消せない?)

総評

色々面倒なのでLinux環境を使うべし

STM32Lシリーズで遊ぶ -DMAを使い倒す①UART

STM32マイコンの情報は今や簡単に見つかると思いますが,HALを使わないレジスタ直たたきコーディングについてのサイトをあまり見かけないと知人から言われたのでチョット何か書いてみようかなと思い至った次第.

私はロボコンでSTM32マイコンを使うときはHALメインで使ってましたし,実際ロボコン程度であればそっちの方が移植性的にも開発コスト的にもメリットが多いと思います. なので今回は省電力というスコープで話をしていきます.

よってSTM32Lシリーズ固定です.

(最近STM32U5シリーズというさらに省電力のファミリが追加されたのですが、まだ触ってないです...)

対象マイコン

ターゲットのマイコンはとりあえず秋月でもnucleoボードが入手可能(多分)のSTM32L053を例に使います.

www.st.com

https://akizukidenshi.com/catalog/g/gM-08022/

L0x3シリーズのリファレンスは日本語化されているので多少開発しやすいですが,rev2ベースで最新のリファレンスとはかけ離れているので注意が必要です(3敗).

仕様構成

今回は直接省電力とは関わってこないですが,前準備としてDMAをいじっていきます.

でもDMAだけでは何とも面白くないので,ついでにADCとTIMとUARTでも使って周期的にADCで読みだした値をUARTでぶん投げることとしますが,ちょとボリュームが多いので何回かに記事を分けます.

UARTの設定

まずDMAで使う前提なのでDMAのチャネル割り当てを確認します.

f:id:tekkamaki200:20220211104839p:plain
L0x3のリファレンス(RM0367 Rev8)より DMAのチャネル表1
f:id:tekkamaki200:20220211104955p:plain
L0x3のリファレンス(RM0361 Rev8)より DMAのチャネル表2

対象がnucleo64なのでUSBから直接通信できるUSART2を使用します.よって送信に使えるDMAチャネルはChannel4かChannel7です.

今回は後のチャネル割り当ての都合でChannel7を使用します.

チャネルの割り当てはDMA_CSELRレジスタで出来るので以下のようにします.

void DMA_init(){
    RCC->AHBENR |= RCC_AHBENR_DMAEN;
    DMA1_CSELR->CSELR |= (0x4 << DMA_CSELR_C7S_Pos);
}

これでDMAの初期設定は終わりなので次にUARTの設定です.

UARTの通信設定自体はとりあえずよくある以下の設定に合わせます.

  • 9600bps
  • 8bit
  • parity none
  • stop bit 1

そんでとりあえずオーバーサンプリングは16にしときます.

void UART_init(){
    /**
    * UART2 ->  9600bps
    * TX -> DMA1_CH7
    */

    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    USART2->BRR = APB_FREQENCY / 9600;

    /**
    * 8bit
    * parity none
    * stop1
    * over16
    * interrupt all disable
    * DMA Tx enable
    */
    USART2->CR1 |=
            USART_CR1_TE |
            USART_CR1_UE;
    //USART2->CR2;  //default
    USART2->CR3 |=
            USART_CR3_DMAT;
}

レジスタのリセット値で問題ないフラグについては何も記述していないので,別の設定にしたい場合はCRレジスタ当たりをいい感じにしてください.送信設定しかしていないのでこのままでは受信もできません.

さて,これで初期設定が終わったので実際にUARTでDMAを使用していきます.

DMAでUSRT送信

ここからが本題です.

STM32のDMA転送にはいくつかの種類と機能があります.

  • 転送サイズ
  • ポインタインクリメント
  • 転送方向
  • サーキュラモード
  • メモリ間モード

転送サイズはByte(8bit) / Harf Word(16bit) / Word(32bit)の3種あり,転送元と転送先で別々のサイズにすることも可能です.ただし,転送を行うペリフェラルレジスタによっては特定サイズしないと動かないことがあります.

ポインタインクリメント機能は,複数の連続するアドレスのデータを転送できる機能です.転送元と転送先でそれぞれインクリメントの有無を独立して設定可能です.

転送方向設定はその名の通り転送する向きの設定です.メモリからペリフェラルまたはペリフェラルからメモリに加えてペリフェラルからペリフェラルへの転送をサポートしています.

サーキュラモードは転送を完了したのち,すぐに再度転送を繰り返し行うモードです.これは受信バッファ等によく使われます.対して,サーキュラモードでない場合はワンショットモードと呼ばれたり呼ばれなかったりします.

メモリ間モードはメモリとメモリの間で転送を行うモード...ではないです.転送方向に記述したようにメモリからメモリへの転送をDMAはサポートしていません.(リファレンスに書いてないだけでもしかしたらできるかもしれないけど)
このモードはDMAのトリガをソフトウェアで行うというモードです.本来,DMAのリクエストは各チャネルごとに割り当てられたペリフェラルからのトリガリエストによって発生します.しかし,メモリ間モードを使用することでこのトリガなしに転送を行えます.

とDMAの概要を説明したところでとりあえず設定を関数化しておきます.

typedef enum{
    SizeByte,
    SizeHWord,
    SizeWord
}DMA_size;
typedef enum {
    ToMem,
    ToPeripheral
}DMA_Direction;

void DMA_set(DMA_Channel_TypeDef* DMA_ch, void* address_p, void* address_m){
    DMA_ch->CPAR = (uint32_t)(address_p);
    DMA_ch->CMAR = (uint32_t)(address_m);
}
void DMA_start(DMA_Channel_TypeDef* DMA_ch, DMA_size p_size, DMA_size m_size, DMA_Direction dir, uint32_t size, bool circular, bool inc_p){
    DMA_ch->CCR &= ~DMA_CCR_EN;
    DMA_ch->CNDTR = size;
    DMA_ch->CCR =
                (m_size << DMA_CCR_MSIZE_Pos) |
                (p_size << DMA_CCR_PSIZE_Pos) |
                (DMA_CCR_MINC) |
                (inc_p << DMA_CCR_PINC_Pos) |
                (circular << DMA_CCR_CIRC_Pos) |
                (dir << DMA_CCR_DIR_Pos) |
                (DMA_CCR_EN);
}

アドレスのセットとその他設定及びDMA有効化を分けました.この分け方は大変分かりにくいかと思いますが,レジスタへのアクセス回数をただ減らしたかっただけです. DMA_start()ではメモリのインクリメントだけは使わないシチュエーションがあまり思い浮かばなかったので強制的に設定しています. またDMA_CCR_ENフラグを下ろしているのは,ワンショットモードで転送を終えた際にCNDTRレジスタが0になるのに対し,DMA_CCR_ENが立ち上がりっぱなしになりCNDTRレジスタへの書き込みがロックされるためです.

では実際にUARTを動かします. 今回は8bitデータの送信なので,転送サイズは転送元/先ともにByteを選択.そして転送方向はメモリからペリフェラルになります.サーキュラモードを使うとひっきりなしに連続送信してしまうので今回は使いません.

int main(void)
{
    uint8_t send_data[] = {'H', 'e', 'l', 'l', 'o', '\n', '\r'};

    PWR_init();
    GPIO_init();
    UART_init();
    DMA_init();

    /**
    * DMA set
    */
    DMA_set(DMA_CH_USART2_TX, (void*)&USART2->TDR, (void*)send_data);


    while(true){
        DMA_start(DMA_CH_USART2_TX, SizeByte, SizeByte, ToPeripheral, sizeof(send_data), false, false);
        GPIOA->ODR ^= (0b1 << 5);

        for(uint32_t i = 0 ;i < 1e5; ++i);
    }
}

クロック等の設定は特にしていないのでforで無理やり待ちを作って適当にループを回しています.

これで無事DMAでのUART送信ができました. 次回はTIMとADCを組み合わせてDMA転送を行います.

今回コードはプロジェクトごとgitに上げておきます.

github.com

今年使用した射出機構について

当初は月一で何か書こうと思っていたんですが、面倒になってなにもしないうちに年を越しそうになっていたのでとりあえず何か書いておくかと思った次第。

特にネタも考えていなかったため今年の射出機構について軽く書きます。

特に目新しいものとか参考になるようなものは無いと思います(断言) まあ今後こんなバカ出力でものを飛ばす機会なんて一生訪れないと思うので良い経験?でした。

射出について

射出機構の選択肢は色々ありますし、飛ばす対象やルールによっても変わるかと思います。なんなら来年の学ロボ2022もボールを飛ばす競技ですね。

今年2021ではアローという細長い訳分からん形のものを最小口径160mmかつ可動なポットに入れるというまあまあしんどい競技でした。 我々はアローを射出する機構として俗にモータ直動と呼ばれる機構を採用しました。 これはまっすぐなパイプにラックを張り付けるなどしてモータの回転を直線運動に変えて打ち出すものです。

初期の試作段階では回転投げや、定荷重バネ砲なども作りましたが採用されませんでした。 理由としてはこれらの装填や射出モーション自体が長い為....というのは確か後付けの理由だった気がします。

もはや一年近く経ってしまって記憶がおぼろげですがとりあえず直動が上手くいったから採用したと思われます。 他の機構についてはあんまり記憶に無いですね....現地でタイミングベルトで直動射出機構を作ってる大学を見たときは素直に「頭いいな~~~」って感動してました。

今年のモータ直動

一般論とかは全く知らんので今年行ったことについてのみ話します。 他の射出機構との比較とか書こうと思ったけど物理なんもわからんしめんどくさいので止めた。

使った機構はマブチの775を二つ使い、その軸にピニオンを付けてラックを二面に貼り付けたパイプを挟み込むようにされています。過去のロボコンで見かけたものたちをガッツリ参考にさせていただきました。

f:id:tekkamaki200:20211230233934p:plain:w300
モータ直動の機構概略図

制御としては見たまんまモータでパイプを加速させて、パイプ上に乗せたものを慣性で飛ばすことになります。 モータ直動でネックになるのは

  • ブレ―キ
  • 加速長
  • アクチュエータ出力

になるかと思います。というかどんな射出機構でも大体これ。

モータ直動においては基本的にブレーキもモータが担うことになるので、ブレーキがあるだけで加速長が削がれます。 その点、先述のタイミングベルト機構はブレーキを考えなくていいというのがホントに素晴らしい。

アクチュエータ出力はモータ単体の出力です。 モータ直動における射出の威力(\simeq飛距離)は最終速度になります。これはおおよそ加速長×単体出力と正の相関にあると考えていいでしょう。 そのためモータ直動の理想は、「できるだけ出力の高いモータ」×「できるだけ長い機構」となります。

長ければ長いほど加速長もブレーキ長もとれるので良いです。今年の機構は確かストロークが700ぐらいで加速に350ちょっと使ってたはずです。 0から加速して0に減速させるので加速長とブレーキ長はだいたい同じになります。(機構が上向きなので重力加速度を考慮すると加速長の方が少し長くなる)

それでも700の機構は長すぎるのでモータの出力を上げたかったのですが、ブラシレスは開発コストから断念し、モータを3個に増やすという案は実行されましたがその加速度のラックが耐え切れず引きちぎれたのでぽしゃりました。

今年のモータ直動の制御

ここまでは「それはそう」みたいな情報量0の話でしたのでそろそろ自分のやったことを書きます。

ざっくりとした制御ブロック図を以下に示します。

f:id:tekkamaki200:20211231105707p:plain:w450
モータ直動の制御ブロック図

今年の機体の下レイヤの制御は高度なことをせず、ほぼほぼPIDのカスケードで制御が組まれています。正直、現在のロボコン程度であれば相当込み入った機構を作らない限りこれぐらいの制御系で何とか賄えると思います。(でもステアの操舵のレスポンスとかはかなり微妙だったのでこれだけで良いというわけでもない。私は上のレイヤの人におんぶにだっこだっただけ)

MDが二つに分かれているのは単純に電流量の都合です。片方のモータにつき瞬時最大110A流しています。モータに使っているバッテリは6セル/2600mAh/定常35C/瞬時70Cなので瞬時最大は182Aとなります。これではモータ二つは賄えないのでこの機構だけでバッテリも二本です。

このモータを複数使うモータ直動ではラックの損耗が問題視されることが多いのですが、今回は電流を二つのモータで同期させることでトルクを同期させてラックの損耗を防ぎました。実際シーズン中にラックは微塵も欠けることは無かったです。

他の問題としてはMDが分かれていると通信速度で制御周期上限が決まるのでしんどくないか?という人もいましたがそこは特に不自由しませんでした。 MD間の通通信はCAN1Mbpsで行っていました。MD間といっても一方通行ですが。 モータ駆動ののPWM周波数は20kHz/電流PI制御の周波数は10kHz/速度制御PID及び通信の周波数は500Hzで行っていました。

あと小ネタとして、急加速急減速の為にモータのブラシ損耗が激しくて2週間ぐらいのスパンでモータを交換してたりしました。 一応これじゃいかんと思い、多少の対策として電流指示にステップ限界を設けて電流微分値に上限を設けてブラシの損耗を抑える試みなどをしましたが効果のほどは分かりません。何も対策しなかったときに比べて気持ちモータの寿命は延びた気もしなくはないです。

何も考えないで書き始めたのでとりあえず以上です。 来年こそはなんかちゃんとした技術記事でも書きたいですね....(願望) では良いお年を。

はじめての投稿

はじめまして、鉄火巻キといいます。この記事の投稿現在大学生をやっています。

きっかけはロボコンですが たしなみ程度に電子工作をしたりするので、備忘録 兼 情報発信できたらなと思います。

よろしくお願いします。