Help us understand the problem. What is going on with this article?

俺が考えた最強のSTM32開発環境構築(STM32CubeMX, OpenOCD, Semihosting, VSCode and C++)

この記事はフューチャー2 Advent Calendar 2019の14日目です。
1枚目もありますのでぜひ御覧ください。

はじめに

普段はGoやJavascriptをNeovimやVSCodeで書いていた私ですが、組み込み系はSW4STM32などEclipse系のIDEを使っていました。
しかし、Web系のモダンな開発環境と比較すると、組み込み系の開発環境はまだまだ使いにくい…!
そこで、STM32(に限らず、ARM Cortex-M)の開発環境をVSCodeを中心に構築した記録です。
勢い余ってC++のビルドも出来るようにしました。

結果、IntelliSenseの「強さ」を見せつけられ、更にVSCodeへの愛が深まってしまいました。

ざっくり、以下のような流れです。

  1. ペリフェラルの初期化コードはSTM32CubeMXで生成する
  2. VSCodeでアプリケーションコードをC++で書く
  3. GNU Makeでビルドする
  4. OpenOCDでデバッグする

環境・バージョンなど

  • macOS Mojave : 10.14.6
  • STM32CubeMX : 5.4.0
  • GNU Make : 3.8.1
  • openocd : 0.10.0
  • GNU Arm Embedded Toolchain : 9-2019-q4-major
  • STM32Cube FW_L0 v1.11.2

arm-none-eabi-gccなどのインストール

コンパイラ(gcc, g++)、ライブラリ(newlib)、その他(objcopyなど)をインストールします。
Homebrewで簡単にインストールできます。
私はダウンロードにそこそこの時間がかかりました。

brew tap ArmMbed/homebrew-formulae
brew install arm-none-eabi-gcc

確認

$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

参考: https://github.com/ARMmbed/homebrew-formulae

openocdのインストール

フラッシュ書き込みなどのためのopenocdをインストールします。

brew install open-ocd

確認

$ openocd --version
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html

ちなみにcfgファイルは、
私の環境だと/usr/local/share/openocd/scripts配下にありました。

$ find /usr/local/share/openocd/scripts -name '*stlink*'
/usr/local/share/openocd/scripts/interface/stlink-v2-1.cfg
/usr/local/share/openocd/scripts/interface/stlink-v1.cfg
/usr/local/share/openocd/scripts/interface/stlink-v2.cfg
...

VSCodeに必要なプラグインをいれる

おおよそ以下のプラグインがあれば大丈夫かと思います。

  • C/C++
  • Cortex-Debug

ソフトウェア・プラグインのインストールはここまでです。

STM32CubeMXでコードを生成する

ここからの手順はLinuxでもWindowsでも同じのはずです(未確認)。

今回はターゲットボードにNucleo-L053R8を使用します。
ほぼデフォルトのままですが、クロック設定は少し弄り、FreeRTOSを有効化にしています。
またFreeRTOSの有効化にあわせて、TimebaseをTIM21にしています。

クロック設定は以下の通りです。
Clock settings

またFreeRTOSは以下の設定をデフォルトから変更しています。

Config parameters タブ

  • Kernel settings
    • RECORD_STACK_HIGH_ADDRESS: Enabled
  • Hook function related definitions
    • USE_MALLOC_FAILED_HOOK: Enabled
    • CHECK_FOR_STACK_OVERFLOW: Option1
  • Run time and task stack gathering related definitions
    • USE_TRACE_FACILITY: Enabled
    • USE_STATS_FORMATTING_FUNCTIONS: Enabled

ここまで出来たらコードを生成します。

CubeMXのProject Managerから、以下の項目を設定します。

Projectタブ

  • Application Structure: Advanced
  • Toolchain / IDE: Makefile

Code Generatorタブ

  • Generated files
    • Generate peripheral initialization as a pair of '.c/.h' files per peripheral: Check

VSCodeにプロジェクトを取り込み、ビルド確認

以下のようなディレクトリ構成になっているはずです。

.
├── Core
│   ├── Inc
│   └── Src
├── Drivers
│   ├── CMSIS
│   └── STM32L0xx_HAL_Driver
├── L053_Build_Test.ioc
├── Makefile
├── Middlewares
│   └── Third_Party
├── STM32L053R8Tx_FLASH.ld
└── startup_stm32l053xx.s

ビルドを行うためにVSCodeのTaskを作成します。
コマンドパレットから Tasks: Configure Default Build Tasktask.json の雛形が出来ますので、これを以下のように編集します。
ビルドの並列数 -jオプション は環境にあわせて適宜変更します。
参考: http://lpha-z.hatenablog.com/entry/2018/12/30/231500

.vscode/tasks.json
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558 
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "type": "shell",
            "label": "Build project",
            "command": "make",
            "args": [
                "DEBUG=1",
                "-j3",
                "all"
            ],
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

これでCmd+Shift+Bでビルドできるようになります。

Makefile修正

生成されたMakefileではビルド時のマクロ定義が少し足りないので、追記します。
__weak=...__packed=...が追加分です。
ただ、これが無くてもビルドは通りました。

Makefile
# C defines
C_DEFS =  \
-DUSE_HAL_DRIVER \
-DSTM32L053xx \
'-D__weak=__attribute__((weak))' \
'-D__packed=__attribute__((__packed__))'

フラッシュ書き込み

プログラムの書き込みもTasksに定義してしまいます。

openocdを使って書き込みを行います。

まず、以下のようなcfgファイルを用意します。

openocd.cfg
telnet_port 4444
gdb_port 3333

source [find interface/stlink-v2-1.cfg]
source [find target/stm32l0.cfg]

init

proc flash_elf {elf_file} {
    reset
    halt
    flash write_image erase $elf_file
    verify_image $elf_file
    echo "flash write_image ($elf_file) complete"
    reset
    exit
}

参考: https://ryochack.hatenablog.com/entry/2017/11/03/234216

このファイルを用意すると、以下コマンドで書き込みができるようになります。
elfファイルのパスは適宜変更してください。
デフォルトのMakefileでは、build/{ProjectName}.elfです。

openocd -f ./openocd.cfg -c "flash_elf build/L053_Build_Test.elf"

この書き込みも、Tasksに登録します。
tasks.jsontasksに以下を追加します。

tasks.json
...
{
    "type": "shell",
    "label": "Flash program",
    "command": "openocd -f ./openocd.cfg -c \"flash_elf build/L053_Build_Test.elf\"",
    "problemMatcher": [],
    "group": "none",
}
...

デバッグ設定(Cortex-Debug)

launch.jsonにデバッグ設定を書いていきます。
コマンドパレットから、Debug: Open launch.json -> Cortex-Debug でひな形が出来ます。

これを以下のように編集します。

.vscode/launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Cortex Debug",
            "cwd": "${workspaceRoot}",
            "executable": "./bin/L053_Build_Test.elf",
            "request": "launch",
            "type": "cortex-debug",
            "servertype": "openocd",
            "configFiles": [
                "interface/stlink-v2-1.cfg",
                "target/stm32l0.cfg"
            ],
            "preLaunchTask": "Build project"
        }
    ]
}

編集箇所は以下です。

  • executableのパスを修正
  • serverTypeopenocdに変更
  • configFilesでターゲットボードにあわせたファイルを読み込むよう修正
  • preLaunchでデバッグ前にビルドタスクを走らせるように修正

これで、F5でデバッグが開始出来ます。
Debug

ここまでの手順で開発は開始出来ます。
これ以降はより快適に開発を進めるための設定です。

VSCodeにインクルードパスを設定する

VSCode側にインクルードパスを設定することで、補完をいい感じにします。
コーディングがだいぶ楽になります。

.vscode/c_cpp_properties.json
{
    "configurations": [
        {
            "name": "STM32",
            "includePath": [
                "${workspaceFolder}/**",
                "/usr/local/Cellar/arm-none-eabi-gcc/9-2019-q4-major/gcc/arm-none-eabi/include/**",
                "/usr/local/Cellar/arm-none-eabi-gcc/9-2019-q4-major/gcc/lib/gcc/arm-none-eabi/9.2.1/include/**"
            ],
            "defines": [
                "USE_HAL_DRIVER",
                "STM32L072xx",
                "__weak=__attribute__((weak))"
            ],
            "compilerPath": "/usr/local/bin/arm-none-eabi-gcc",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}

この設定がなくてもデフォルトでworkspace配下のファイルは走査してくれるのですが、ARM-Toolchainのヘッダファイル群も読み込まれるように設定することで更に良い感じになります。(uint32_tで型エラーが出なくなるなど)

また、definesに関してはMakefileのものを転記すれば良いです。
これもビルド時に指定するものはヘッダファイルからは読み取れないので、ここで教えてあげる必要があります。

Semihosting機能を使う

printfを使用するために、Semihosting機能を有効にします。
これにより、UARTを使わなくてもSTM32からホストPCに文字列を出力できるようになります。

Semihostingの詳細は以下を参考にしてください。
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0203ij/Bgbjjgij.html

デバッガ設定の修正

launch.jsonを編集し、デバッガでSemihostingを有効にします。
openocdで、monitor arm semihosting enable というコマンドを実行します。

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Cortex Debug",
            ...
            "postLaunchCommands": [
                "monitor arm semihosting enable"
            ]
        }
    ]
}

リンカオプションの修正

Makefileでリンカオプションを修正します。
CubeMXで生成されたMakefileの場合、修正点は以下の通りです。

Makefile
# libraries
# LIBS = -lc -lm -lnosys <- Comment out
LIBS = -lc -lm -lrdimon
LIBDIR = 
# LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
LDFLAGS = $(MCU) -specs=nosys.specs -specs=rdimon.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections

ソースコードの修正

main.cで、Semihostingの初期化を行います。
修正は2箇所です。

まず、関数のプロトタイプ宣言に以下を追加します。

main.c
extern void initialise_monitor_handles(void);

次に、この関数をmain()の頭で呼びます。

main.c
int main(void)
{
  /* USER CODE BEGIN 1 */
  initialise_monitor_handles();
...

以上でprintf()が使えるようになります。

あとは、以下のようにprintfを仕込めば、

main.c
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
printf("Peripheral initialization completed.\n");

以下のようにOutputウィンドウ(Cmd+Shift+Uで開きます)に文字列が出力されます。

semihosting is enabled
Peripheral initialization completed.

なお、改行コードが来るまでOutputウィンドウに文字列は出力されないのでご注意ください。

C++を書けるようにする

CubeMXで生成されたコードはすべてCなのですが、
追加のアプリケーションコードはC++で書きたいので、諸々の設定をしていきます。
ちなみに、CubeMXの各ヘッダファイルはextern "C"がきちんと付いているので、ビルドの設定だけきちんとしてあげれば比較的簡単に移行が可能です。

MakefileをC++のコンパイル・リンクが出来るように修正します。
まずはコンパイラのパスを指定します。

Makefile
#######################################
# binaries
#######################################
PREFIX = arm-none-eabi-
# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH
CC = $(GCC_PATH)/$(PREFIX)gcc
CXX = $(GCC_PATH)/$(PREFIX)g++    # 追加
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(GCC_PATH)/$(PREFIX)objcopy
SZ = $(GCC_PATH)/$(PREFIX)size
else
CC = $(PREFIX)gcc
CXX = $(PREFIX)g++    # 追加
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif

次に良い感じにC++のビルド変数を追加していきます。

Makefile
# CPP sources
CPP_SOURCES = \
$(wildcard Application/src/*.cpp) \

# C++ includes
CPP_INCLUDES = $(C_INCLUDES)
CPP_INCLUDES += \
-IApplication/inc

# compile g++ flags
CPPFLAGS = $(MCU) $(CPP_DEFS) $(CPP_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections

# Generate dependency information
CPPFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"

CとC++が混在することになるため、明示的にlibstdc++をリンクさせます。

Makefile
# libraries
LIBS = -lc -lm -lrdimon
LIBS += -lstdc++

ビルド対象のリストにC++ソースを追加します。

Makefile
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of c++ objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(CPP_SOURCES:.cpp=.o)))  # 追記
vpath %.cpp $(sort $(dir $(CPP_SOURCES)))  # 追記

C++ソースのコンパイルが行われるようにルールを追加します。

Makefile
$(BUILD_DIR)/%.o: %.cpp Makefile | $(BUILD_DIR) 
    $(CXX) -c $(CPPFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.cpp=.lst)) $< -o $@

リンクをg++で行うように修正します。

Makefile
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
    $(CXX) $(OBJECTS) $(LDFLAGS) -o $@  # 修正
    $(SZ) $@

以上で、C++が書けるようになりました。

C++のソースを書いてみる

例として、LED制御のコードをC++で書いてみます。

まずはヘッダファイル。
void led_c_wrapper_example(void)は、Cから呼び出すためのラッパーです。

Application/inc/led.h
#ifndef __led_H
#define __led_H

#include "stm32l0xx_hal.h"

class led
{
private:
    GPIO_TypeDef *port;
    uint16_t pin;

public:
    led(GPIO_TypeDef *_port, uint16_t _pin);

    void turnOn(void);
    void turnOff(void);
};

extern "C" void led_c_wrapper_example(void);

#endif

次にソースファイルです。

Application/src/led.cpp
#include "led.h"

#include <stdio.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"

led::led(GPIO_TypeDef *_port, uint16_t _pin) : port(_port), pin(_pin)
{
}

void led::turnOn(void)
{
    HAL_GPIO_WritePin(this->port, this->pin, GPIO_PIN_SET);
}

void led::turnOff(void)
{
    HAL_GPIO_WritePin(this->port, this->pin, GPIO_PIN_RESET);
}

void led_c_wrapper_example(void)
{
    led ld2(LD2_GPIO_Port, LD2_Pin);

    for (;;)
    {
        printf("Toggle LED\n");
        ld2.turnOn();
        vTaskDelay(pdMS_TO_TICKS(500));
        ld2.turnOff();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}


あとは、このラッパー関数をfreertos.cあたりから呼び出します。

Core/Src/freertos.c
extern void led_c_wrapper_example(void);

void StartDefaultTask(void const *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
  led_c_wrapper_example();
  /* Infinite loop */
  for (;;)
  {
    osDelay(100);
  }
  /* USER CODE END StartDefaultTask */
}

ディレクトリ構成を整える

前の手順でしれっとApplicationというディレクトリを作りましたが、その中身は以下のようになっています。

Application
├── inc
│   └── led.h
└── src
    └── led.cpp

CubeMXの自動生成コードと、自分で書いたコードは、きっちりディレクトリを分けておくのが良いかと思います。

まとめ

上記の設定で、IntelliSenseをバリバリに効かせながら、C++を使って快適にSTM32の開発が出来るようになったはずです。
ユニットテストまで出来ればより良いのですが、まだ最高の環境に辿り着けていません…。

今回使ったコードはすべて以下のリポジトリにアップしています。
https://github.com/uhey22e/stm32_with_vscode_example/

また、以下のページでDiffを見ると、CubeMXのデフォルトからどこをいじったのかがわかりやすいかと思います。
https://github.com/uhey22e/stm32_with_vscode_example/compare/cubemx_default...master
たまにVSCodeのフォーマッターのせいで本筋と関係のないところにDiffが出ていますがご容赦ください。

次はRustかなあ…

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした