この記事はフューチャー2 Advent Calendar 2019の14日目です。
1枚目もありますのでぜひ御覧ください。
はじめに
普段はGoやJavascriptをNeovimやVSCodeで書いていた私ですが、組み込み系はSW4STM32などEclipse系のIDEを使っていました。
しかし、Web系のモダンな開発環境と比較すると、組み込み系の開発環境はまだまだ使いにくい…!
そこで、STM32(に限らず、ARM Cortex-M)の開発環境をVSCodeを中心に構築した記録です。
勢い余ってC++のビルドも出来るようにしました。
結果、IntelliSenseの「強さ」を見せつけられ、更にVSCodeへの愛が深まってしまいました。
ざっくり、以下のような流れです。
- ペリフェラルの初期化コードはSTM32CubeMXで生成する
- VSCodeでアプリケーションコードをC++で書く
- GNU Makeでビルドする
- 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にしています。
また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 Task
で task.json
の雛形が出来ますので、これを以下のように編集します。
ビルドの並列数 -jオプション
は環境にあわせて適宜変更します。
参考: http://lpha-z.hatenablog.com/entry/2018/12/30/231500
{
// 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=...
が追加分です。
ただ、これが無くてもビルドは通りました。
# C defines
C_DEFS = \
-DUSE_HAL_DRIVER \
-DSTM32L053xx \
'-D__weak=__attribute__((weak))' \
'-D__packed=__attribute__((__packed__))'
フラッシュ書き込み
プログラムの書き込みもTasksに定義してしまいます。
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.json
のtasks
に以下を追加します。
...
{
"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
でひな形が出来ます。
これを以下のように編集します。
{
// 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
のパスを修正 -
serverType
をopenocd
に変更 -
configFiles
でターゲットボードにあわせたファイルを読み込むよう修正 -
preLaunch
でデバッグ前にビルドタスクを走らせるように修正
ここまでの手順で開発は開始出来ます。
これ以降はより快適に開発を進めるための設定です。
VSCodeにインクルードパスを設定する
VSCode側にインクルードパスを設定することで、補完をいい感じにします。
コーディングがだいぶ楽になります。
{
"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の場合、修正点は以下の通りです。
# 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箇所です。
まず、関数のプロトタイプ宣言に以下を追加します。
extern void initialise_monitor_handles(void);
次に、この関数をmain()
の頭で呼びます。
int main(void)
{
/* USER CODE BEGIN 1 */
initialise_monitor_handles();
...
以上でprintf()
が使えるようになります。
あとは、以下のようにprintfを仕込めば、
/* 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++のコンパイル・リンクが出来るように修正します。
まずはコンパイラのパスを指定します。
#######################################
# 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++のビルド変数を追加していきます。
# 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++をリンクさせます。
# libraries
LIBS = -lc -lm -lrdimon
LIBS += -lstdc++
ビルド対象のリストにC++ソースを追加します。
# 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++ソースのコンパイルが行われるようにルールを追加します。
$(BUILD_DIR)/%.o: %.cpp Makefile | $(BUILD_DIR)
$(CXX) -c $(CPPFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.cpp=.lst)) $< -o $@
リンクをg++
で行うように修正します。
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
$(CXX) $(OBJECTS) $(LDFLAGS) -o $@ # 修正
$(SZ) $@
以上で、C++が書けるようになりました。
C++のソースを書いてみる
例として、LED制御のコードをC++で書いてみます。
まずはヘッダファイル。
void led_c_wrapper_example(void)
は、Cから呼び出すためのラッパーです。
#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
次にソースファイルです。
#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
あたりから呼び出します。
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かなあ…