HALとMakefileの構成でC++を使う

はじめに

STM32の開発を行う際は,いくつかの選択肢があります(ありました).

  • 生レジスタを叩く
  • mbedを使う
  • SPL(Standard Peripheral Library)を使う
  • HAL(Hardware Abstraction Layer) Driversを使う

僕はこれまで,SPLを使って開発を行っていましたが,SPLの開発が終了してしまいました.これ以降に発表されるマイコンではSPLが使えなくなります.
そこで,SPLからHALに乗り換えすることにしました.
この記事では,Linuxで,統合開発環境を使わず,C++(gcc)でHALを使った開発を行う手順を説明します.環境さえ整えばWindowsやMacでも開発できると思います.
おまけとして,複数のFTDIチップを利用する場合のTipsを書こうと思います.

今回僕が使っている環境は,次のとおりです.

  • マイコン:STM32F412CG
  • 書き込み方法:USART1(USBシリアル変換ICを利用)
  • 開発機OS:Arch Linux

準備

STM32CubeMX

まずは必要なファイルを入手します.

  1. STM32CubeMX
    STM32用のコードジェネレータです.今回は,ピン配置の確認・Makefileの生成・サンプルプログラムの生成に利用します.
  2. UM1725: Description of STM32F4 HAL and LL drivers
    HAL Driversのマニュアルです.割と見やすいので使い倒しましょう.

どちらも,次のURLから入手することが出来ます.

STM32CubeF4 - Embedded software for STM32F4 series - STMicroelectronics

開発環境

開発環境を整えます.

以上をインストールしておいてください.

Arch Linuxなら次のコマンドでインストールできます.

1
2
pacman -S make emacs arm-none-eabi-gcc
yaourt -S stm32flash

stm32flashの改造

stm32flashのデバイス設定ファイルが2016年5月から更新されておらず,これ以降に発表されたマイコンに書き込むことが出来ません.
そこで,ソースコードを変更してコンパイルし直しました.

今回はSTM32F412を追加してみます.

dev_table.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -42,6 +42,7 @@ static uint32_t p_1k[]  = { SZ_1K,  0 };
static uint32_t p_2k[] = { SZ_2K, 0 };
/* F2 and F4 page size */
static uint32_t f2f4[] = { SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0 };
+static uint32_t f2f4_2[] = { SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, SZ_128K, SZ_128K, SZ_128K, SZ_128K, SZ_128K, 0 };
/* F4 dual bank page size */
static uint32_t f4db[] = {
SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, SZ_128K,
@@ -86,6 +87,7 @@ const stm32_dev_t devices[] = {
{0x433, "STM32F401xD(E)" , 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0},
{0x458, "STM32F410xx" , 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0},
{0x431, "STM32F411xx" , 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0},
+ {0x441, "STM32F412xx" , 0x20003000, 0x20040000, 0x08000000, 0x08100000, 1, f2f4_2, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0},
{0x421, "STM32F446xx" , 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4 , 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0},
{0x434, "STM32F469xx" , 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db , 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, 0x1FFF7800, 0},
/* F7 */

コード生成する

これで環境が整ったので,コード生成します.STM32CubeMXを起動して,順に作業します.

ピン配置を決める

Pinoutタブから,マイコンのピン配置を決めます.左側の機能一覧から割り当てたり,右のピン一覧から決めたりすることが出来ます.大体の操作は見たとおりにできると思います.
1個だけわかりにくかった設定は,エンコーダのAB相入力です.画像の赤枠部分から設定できるので,参考にしてください.

STM32CubeMXでピン配置を設定

クロックを設定する

STM32CubeMXがクロックの初期化コードを作ってくれます.Clock Configurationタブから,画像の赤枠部分を変更します.HSE(外部クロック)を使う場合は,その周波数も入力しておきましょう.

クロックの設定箇所

機能の設定

SPIやTIMなどを利用する場合,パラメータの設定をしておきます.Configurationタブから,なんかいい感じに設定しておきましょう.

SPI2の設定をする

出力ファイルの設定

最後に,プロジェクト全体の設定をします.今回の記事で重要な点は,画像の赤線部分です.この通り設定し,出力パスや名前を各自設定しておきましょう.

Makefileを利用したプロジェクトとする
細かい設定もしておく

出力する

では,Generate Codeを実行しましょう.僕の例では,次のようなファイルが出力されました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.
├── Drivers (このディレクトリにはHAL Driversが入っている.長いので省略)
├── Inc
│   ├── adc.h
│   ├── dma.h
│   ├── gpio.h
│   ├── main.h
│   ├── spi.h
│   ├── stm32f4xx_hal_conf.h
│   ├── stm32f4xx_it.h (itはinterruptの略? 割り込み関数が宣言されている)
│   ├── tim.h
│   └── usart.h
├── Makefile (最高)
├── STM32F412CGUx_FLASH.ld
├── STM32F412CG_FLASH.ld
├── Src
│   ├── adc.c
│   ├── dma.c
│   ├── gpio.c
│   ├── main.c
│   ├── spi.c
│   ├── stm32f4xx_hal_msp.c
│   ├── stm32f4xx_it.c (割り込み関数の定義がしてある)
│   ├── system_stm32f4xx.c
│   ├── tim.c
│   └── usart.c
├── Tanitan_v3.0H.elf.launch
├── Tanitan_v3.0H.ioc
├── Tanitan_v3.0H.pdf (Generate Reportにより生成.非常に便利)
├── Tanitan_v3.0H.txt
├── startup
│   └── startup_stm32f412cx.s (スタートアップルーチン.クロック設定もここで実行される)
└── startup_stm32f412cx.s

Generate Reportを実行すると,今回設定した情報がpdfで吐き出されます.非常に便利なので出力することをオススメします.

C++でコンパイルを通す

さてさて,コードが生成されたところで,g++でコンパイルを通せるように改造していきましょう.

今回の記事では,コード生成し直さないことを前提に進めていきます.そのため,生成されたコードを開発用のディレクトリにコピーしてしまうことをオススメします.
生成コードの中で,次のファイルは使いません.

  • Inc 以下の,stm32f4xx_hal_conf.h と stm32f4xx_it.h 以外のファイル
  • Src 以下の,stm32f4xx_hal_msp.c と stm32f4xx_it.c と system_stm32f4xx.c 以外のファイル

正確には,あくまで初期設定のサンプルとして参考にするだけで,実装は自分で行うことになります.効率はあまり良くないですが,自動生成のコードがあまり好きではないため,このように進めていきます.

ツールチェインのパスを設定する

自動生成コードそのままでは,コンパイルが通りません.まずはツールチェインのディレクトリを設定します.

1
2
3
4
5
6
7
8
9
10
11
12
#######################################
# binaries
#######################################
BINPATH = /usr/bin
PREFIX = arm-none-eabi-
CC = $(BINPATH)/$(PREFIX)g++
AS = $(BINPATH)/$(PREFIX)g++ -x assembler-with-cpp
CP = $(BINPATH)/$(PREFIX)objcopy
AR = $(BINPATH)/$(PREFIX)ar
SZ = $(BINPATH)/$(PREFIX)size
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S

おそらくこの状態でコンパイル自体は通ると思います.

cppファイルをコンパイルできるようにする

今は,コンパイルする.cファイルをすべて列挙してあります.非常に気に食わないので,Srcディレクトリ以下に存在する.c,.cppファイルをすべてコンパイルするように変更します.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
######################################
# source
######################################
# C sources
C_SOURCES = \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_adc.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_adc_ex.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_spi.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim_ex.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.c \
Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.c \
$(wildcard Src/*.c)

# CPP sources
CPP_SOURCES = \
$(wildcard Src/*.cpp)

# ASM sources
ASM_SOURCES = \
startup_stm32f412cx.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#######################################
# build the application
#######################################
# list of CPP program objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(CPP_SOURCES:.cpp=.o)))
vpath %.cpp $(sort $(dir $(CPP_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
# list of C program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))

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

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

$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)
$(AS) -c $(CFLAGS) $< -o $@

こんな風に改造することで,ビルドが通ると思います.

C++でコードを書き直す

あとは,好きに書き直すだけです.
自動生成されたコードは(ほとんど)正しいので,僕はそれを参考にしながら機能ごとにクラス化していきました.

リンカエラーに対処する

C++でコンパイルするとリンカエラーがしばしば発生しました.
このあたりはよくわかっていませんが,リンカオプションを付け加えることで対処していました.参考までに僕のリンカオプションを書き残しておきます.

1
2
3
4
5
6
7
8
9
10
11
#######################################
# LDFLAGS
#######################################
# link script
LDSCRIPT = STM32F412CGUx_FLASH.ld

# libraries
LIBS = -lc -lm -lnosys -lgcc -lrdimon -lstdc++

LIBDIR = -Llibmouse -Llibpath
LDFLAGS = $(MCU) -specs=nano.specs -specs=rdimon.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections -u _printf_float

まとめ

統合開発環境を使わずに,STM32CubeMXの自動生成コードをC++に対応させました.
ぶっちゃけ統合開発環境使った方がお手軽ですが,楽しかったので良しとします.

おまけ

今年作った書き込み基板,その名も「メジロ」です.

書き込み基板ですが,USBシリアル変換以外にも様々な機能を搭載しています.その都合で,メジロの中にFT231(USB-USART変換IC)が2個載っています.
Linuxでは/dev/ttyUSB0からUSBデバイスを接続した順番に数字が割り振られていくため,複数のUSBデバイスを接続する際には問題になります.おまけでは,解決方法についてまとめます.

まず,FTDIのサポートページからFT_Progをダウンロード,インストールします.Windows専用なので諦めてパソコンを引っ張り出してきてください.

FTDI Utilities

詳しい使い方はユーザーガイドを見てください.このソフトを使って,複数のFTDIチップのシリアルナンバーを変更します.仮に,2つのチップに”ABCDEFGH”と”12345678”を設定したとします.

では両方ともPCに接続します.正しくシリアルナンバーを設定できていれば,/dev/serial/by-id 以下に
“usb-FTDI_FT230X_Basic_UART_ABCDEFGH-if00-port0”
“usb-FTDI_FT230X_Basic_UART_12345678-if00-port0”
というリンクが張られています.この名前は不変です./dev/ttyUSBx の代わりにこちらを使うようにしましょう.

結論:便利!!