ここ数年は STM32 を使用したプロジェクトを中心に開発してきました。知見が貯まってきているのでまとめておきます。

今回は初歩的なところから、

プロジェクト生成 実行/デバッグ テスト について書いていきます。

ここで作成したプロジェクトは以下のレポジトリに置いています。実際に動作させながら読むと理解しやすいはずです。

https://github.com/jfcamel/stm32f4_cubemx_template



TOC

必要なもの

ハードウェア

ソフトウェア

STM32 CubeMX

https://www.st.com/ja/development-tools/stm32cubemx.html

STM32CubeMXは、STMCubeTMベースの初期化コード自動生成ツールで、開発の工数や時間、コストの削減に貢献し、開発を簡略化します。STM32CubeはSTM32ポートフォリオのすべてに対応します。

とあるように最初にプロジェクトを作成するのにとても有用です。Library などを自動で download してくれます。また視覚的にシステムクロック回路や周波数、各周辺機器の制御が可能です。初期化コード生成/編集ができます。

CMake

https://cmake.org/

Makefile を生成してくれる cross platform な tool. OS やシステム内のライブラリの配置場所の差を吸収してくれます。

GNU Arm Embedded Toolchain

https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm

Arm 向けの toolchain. gcc や libc, binutils などをセットにしたもの。 download してPATH を通しておきましょう。

OpenOCD

http://openocd.org/

STM32 ボードに Debug 線で接続するのに使用します。バイナリを書き込む、標準出力を確認する、デバッグする、などが可能になります。

CppUTest

https://github.com/cpputest/cpputest

C/C++ プロジェクトで使用できるテストライブラリ。他にも候補となるライブラリはあるとは思いますが、機能とビルド後のサイズの大きさなどから採用します。

プロジェクト生成

CubeMX を起動して

File -> New Project

画面左の MCU/MPU Filters から

Core: Arm Cortex-M4 Series: STM32F4

を選択し、 画面右下の

STM32F411RE

今回のボードの MCU を選択します。

画面右上に特徴やブロック図、ドキュメント、データシートばかりか購入までできます。 ここで “Start Project” します。

“Pinout & Configuration”, “Clock Configuration”, “Project Manager”, “Tools” とタブ があります。“Project Manager” へ進みます。

左側の青い部分が縦のタブになってます。

縦タブの “Project” -> “Project Settings”

Project Name => 任意の名前を入力

Project Location => 任意の場所を入力

Toolchain/IDE => Makefile

縦タブの “Advanced Settings” -> “Driver Selector”

“RCC” を “HAL” から “LL” に変更。

これは提供される API の実装レベルをより Low Layer にしています。

後は右上に見える “GENERATE CODE” ボタンを押下すればプロジェクトが生成され、プロジェクトのルートで “make” を実行すれば問題なくbuild できます。

プロジェクトのファイル構成は以下のようになっています。

. 
./project1.ioc
./Core
./Drivers
./startup_stm32f411xe.s
./STM32F411RETx_FLASH.ld
./.mxproject
./Makefile

この構成はプロジェクト構成ファイルや提供されるライブラリ、プロジェクトソースコードなどです。

“project1.ioc” が STM32CubeMX のプロジェクト構成ファイルです。再度 CubeMX からプロジェクトの編集が可能です。 プロジェクトのソースコードは以下のようになります。

Core/
Core/Src
Core/Src/system_stm32f4xx.c
Core/Src/stm32f4xx_it.c
Core/Src/main.c
Core/Inc
Core/Inc/stm32f4xx_it.h
Core/Inc/stm32_assert.h
Core/Inc/main.h

今後編集していくのはここのファイルになります。 提供されるライブラリは以下のようになります。


Drivers/
Drivers/STM32F4xx_HAL_Driver
Drivers/CMSIS

STM32F4xx_HAL_Driver 以下に配置されるコードを使用すると STM32 の機能を操作することができます。

以下のファイルがメインとなるヘッダファイルです。

“Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h”

STM32F4シリーズにもバリエーションがあるので Makefile 内で “STM32F411xE” を define することで必要なヘッダファイルを include してくれます。

その他は詳細には触れません。

実行/デバッグ

USB 接続

openocd を使用して SWD(Single Wire Debug) に接続します。

STM32f411RE nucleo に USB を接続します。udev の rule ファイルに以下を記述しておけばserial としてアクセスできるようになります。権限などは適宜調整してください。

# 例

USBSYSTEM="tty", ATTRS{idProduct}=="374b", ATTRS{"idVender"}=="0483", GROUP="dialout"


$ lsusb

...

Bus 001 Device 021: ID 0483:374b STMicroelectronics ST-LINK/V2.1

...


$ ls -al /dev/tty*

...

crw-rw-rw- 1 root dialout 188,  0 Jan  5 19:53 /dev/ttyUSB0

...

ここまで認識されていれば以下のコマンドで SWD 接続ができます。

ここで openocd を実行するディレクトリは重要なので覚えて置きます。プロジェクトルートで実行するとわかりやすいです。


$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
Open On-Chip Debugger 0.10.0+dev-01514-ga8edbd020-dirty (2020-11-17-14:44)
Licensed under GNU GPL v2
For bug reports, read
       http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 2000 kHz
Info : STLINK V2J28M17 (API v2) VID:PID 0483:374B
Info : Target voltage: 0.007879
Error: target voltage may be too low for reliable debugging
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections

openocd が接続されると localhost:4444 に telnet でコマンド制御できます。

$ telnet localhost 4444
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
>

ファームウェア書き込み

次にファームウェアを書き込みます。接続したデバイスを halt させて FLASH へ書き込み、reset して再実行させます。


> reset halt
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
target halted due to debug-request, current mode: Thread  
xPSR: 0x01000000 pc: 0x08000608 msp: 0x20020000
> flash write_image erase build/project1.bin 0x08000000
device id = 0x10006431
flash size = 512 kbytes
auto erase enabled
wrote 16384 bytes from file build/project1.bin in 0.649549s (24.632 KiB/s)

> reset run
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
target halted due to breakpoint, current mode: Thread  
xPSR: 0x61000000 pc: 0x08002470 msp: 0x2001ffb8
>


flash コマンドには write_image サブコマンドで、引数で “erase”, バイナリファイル, 書き込みアドレス を渡しています。

STM32f411では FLASH メモリがいくつかのセクタに分かれており、書き込む前に erase するためのオプションと、openocd の実行ディレクトリからの相対パスかシステム上の絶対パス、内蔵の FLASH メモリが 0x08000000 ~ 0x0807FFFF (512KB) にマッピングされているので 0x08000000 を指定します。

ここまでの操作が正しく行われていれば、reset run した後に正しく実行されてます。しかしこのままだと確認ができないですよね。

ARM Semihosting

https://developer.arm.com/documentation/dui0203/j/semihosting/about-semihosting/what-is-semihosting-

OpenOCD で SWD 接続時であれば ARM semihosting で標準出力を確認することができます。編集して標準出力に文字列が表示されるようにします。

流れとしては、

  • main.c で printf を使って標準出力に文字列を表示

  • Makefile 内の Linker option で ARM Semihosting に対応した library を使用するように変更

  • telnet で arm semihosting enable

となります。 以下 diff です。

modified   Core/Src/main.c
@@ -22,6 +22,7 @@
 
 /* Private includes ----------------------------------------------------------*/
 /* USER CODE BEGIN Includes */
+#include <stdio.h>
 /* USER CODE END Includes */
 
 /* Private typedef -----------------------------------------------------------*/
@@ -62,6 +63,7 @@ extern void initialise_monitor_handles(void);
 int main(void)
 {
   /* USER CODE BEGIN 1 */
+  initialise_monitor_handles();
   /* USER CODE END 1 */
 
   /* MCU Configuration--------------------------------------------------------*/
@@ -96,6 +98,7 @@ int main(void)
   while (1)
   {
     /* USER CODE END WHILE */
+    printf("Hello world\n");
     /* USER CODE BEGIN 3 */
   }
   /* USER CODE END 3 */
modified   Makefile
@@ -137,9 +137,11 @@ CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)"
 LDSCRIPT = STM32F411RETx_FLASH.ld
 
 # libraries
-LIBS = -lc -lm -lnosys 
+LIBS = -lm -lnosys
+#LIBS = -lc -lm -lnosys 
 LIBDIR = 
-LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
+#LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections
+LDFLAGS = $(MCU) -specs=rdimon.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections -u _scan_float -u _printf_float
 
 # default action: build all
 all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

再度 make を実行してバイナリを更新して、書き込みます。telnet で reset run を実行する前に arm semihosting enable を実行します。

> reset halt                                            
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
target halted due to debug-request, current mode: Thread  
xPSR: 0x01000000 pc: 0x08000608 msp: 0x20020000, semihosting
> flash write_image erase build/project1.bin 0x08000000
auto erase enabled
wrote 16384 bytes from file build/project1.bin in 0.707962s (22.600 KiB/s)

> arm semihosting enable                                
semihosting is enabled

> reset run                                             
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
> 

openocd を実行したコンソールで以下を確認できます。

Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
target halted due to debug-request, current mode: Thread  
xPSR: 0x01000000 pc: 0x08000608 msp: 0x20020000, semihosting
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world

...

“Hello world” を確認できるまで正しく動作しているか判断できなかったと思います。

LED で視覚的に確認ができる 無線・有線でデータを受信できる 電圧/電流を機器で確認できる こういった機能ではない場合、組み込み開発では正しいファームウェアを書き込んでも動作の確認ができないことが多いです。あらかじめ動作を確認できるような環境を整備しておくと安心できます。テストを書いておく意義がここにあります。次にテスト環境の構築へ進みます。

Linker オプションの rdimon.specs では printf が含まれるコードでは実行速度で影響があるので実行速度の要件があるような場合はこのまま Release 用バイナリとしては問題があります。テスト時のみなど用途を考慮したほうが良いです。

GDBデバッグ

openocd が起動している間 3333 port で gdb が接続できます。

$ arm-none-eabi-gdb build/project1 
GNU gdb (GNU Arm Embedded Toolchain 9-2020-q2-update) 8.3.1.20191211-git
Copyright (C) 2019 Free Software Foundation, Inc.                                                                                                                                                                                
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-linux-gnu --target=arm-none-eabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
   <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from build/project1...
(gdb) target remote :3333
Remote debugging using :3333
0x0800905e in ?? ()
(gdb) load
Loading section .isr_vector, size 0x198 lma 0x8000000
Loading section .text, size 0xc64 lma 0x8000198
Loading section .rodata, size 0x80 lma 0x8000dfc
Loading section .init_array, size 0x4 lma 0x8000e7c
Loading section .fini_array, size 0x4 lma 0x8000e80
Loading section .data, size 0x68 lma 0x8000e84
Start address 0x8000330, load size 3820
Transfer rate: 7 KB/sec, 636 bytes/write.
(gdb)

後は gdb のコマンドで操作できます。

テスト

CMake 管理への移行

テストにすぐ取りかかりたいところですが、その前に Makefile 管理のプロジェクトを CMakefile へ移行したいと思います。

Makefile の記述をそのまま CMakeLists.txt へ。

cmake_minimum_required(VERSION 3.10)
get_filename_component(PROJ ${CMAKE_CURRENT_LIST_DIR} NAME)
project(${PROJ})
SET(PREFIX arm-none-eabi)
SET(CMAKE_C_COMPILER ${PREFIX}-gcc)
SET(CMAKE_CXX_COMPILER ${PREFIX}-g++)
SET(CMAKE_ASM_COMPILER ${PREFIX}-as)
SET(CMAKE_OBJCPY ${PREFIX}-objcopy)
set(CPU "-mcpu=cortex-m4")
set(FPU "-mfpu=fpv4-sp-d16")
set(FLOAT-ABI "-mfloat-abi=hard")
set(MCU "${CPU} -mthumb ${FPU} ${FLOAT-ABI}")
set(DEBUG "-g1")
set(OPTIMIZE "-Os")
set(CMAKE_C_FLAGS "${MCU} ${OPTIMIZE} ${DEBUG} -ffunction-sections -fdata-sections -Wall")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -std=c++11 -fpermissive -fexceptions -fnon-call-exceptions -fno-rtti -fno-use-cxa-atexit -fno-common")
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_FLAGS} -T${CMAKE_CURRENT_LIST_DIR}/STM32F411RETx_FLASH.ld  --specs=rdimon.specs -u _scan_float -u _printf_float -Wl,--gc-sections")
set(C_SOURCES
  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/main.c
  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/stm32f4xx_it.c
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_gpio.c
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_rcc.c
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_utils.c
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_exti.c
  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/system_stm32f4xx.c
  )
set(ASM_SOURCES
  ${CMAKE_CURRENT_SOURCE_DIR}/startup_stm32f411xe.s
  )
set_property(SOURCE
  ${CMAKE_CURRENT_SOURCE_DIR}/startup_stm32f411xe.s PROPERTY LANGUAGE C)
add_executable(${PROJ}
  ${C_SOURCES}
  ${ASM_SOURCES}
  )
target_include_directories(${PROJ} PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Inc/
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/CMSIS/Include/
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F4xx/Include/
  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Inc/
  )
target_compile_definitions(${PROJ} PRIVATE
  -DUSE_FULL_LL_DRIVER
  -DHSE_VALUE=25000000
  -DHSE_STARTUP_TIMEOUT=100
  -DLSE_STARTUP_TIMEOUT=5000
  -DLSE_VALUE=32768
  -DEXTERNAL_CLOCK_VALUE=12288000
  -DHSI_VALUE=16000000
  -DLSI_VALUE=32000
  -DVDD_VALUE=3300
  -DPREFETCH_ENABLE=1
  -DINSTRUCTION_CACHE_ENABLE=1
  -DDATA_CACHE_ENABLE=1
  -DSTM32F411xE
  )
target_link_libraries(${PROJ}
  -lm -lnosys
  )
add_custom_target(${PROJ}.bin ALL
  COMMAND
  ${CMAKE_OBJCPY} -O binary ${CMAKE_CURRENT_BINARY_DIR}/${PROJ} ${CMAKE_CURRENT_BINARY_DIR}/${PROJ}.bin
  )
add_dependencies(${PROJ}.bin ${PROJ})

CMake 構文については説明しませんが、変更点や気をつけるところは以下です。

project 名をディレクトリ名を自動でセットするように変更 set_property で asm ファイルが C SOURCE としてコンパイルされるように変更 CFLAG に渡す Optimize flag を-Os としサイズ優先へ Linker オプションで rdimon.specs を指定 CMake 移行後のビルド手順は以下となります。


$ mkdir build && cd build

$ cmake ..

$ make

これは必要ソフトウェアで挙げた GNU Arm Embedded Toolchain が PATH に通っていないとビルド出来ません。

次に CMake にテスト用の target を追加していきます。

テストバイナリの生成 test ディレクトリを作成し、そこに CMakeLists.txt を配置し、テストコードも用意します。

± find test/
test/
test/Src
test/Src/main.cpp
test/CMakeLists.txt

プロジェクトルートの CMakeLists.txt には以下の内容を追加します。

add_subdirectory(test)

test/CMakeLists.txt では、まず CPPUTEST のファイルを変数に格納します。環境変数 CPPUTEST_HOME は適宜書き換えてください。ARM 用に事前コンパイルしていない限りは一緒にコンパイルする必要があるのでソースを用います。CMakeLists.txt 内で Library として定義して共通化することもできますが、CFLAGS の変更や DEFINITION の変更に対応するのには都度コンパイルした方が単純で把握しやすいです。

SET(_CPPUTEST_SOURCES
  $ENV{CPPUTEST_HOME}/src/CppUTest/CommandLineArguments.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/CommandLineTestRunner.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/JUnitTestOutput.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/MemoryLeakDetector.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/MemoryLeakWarningPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestTestingFixture.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/Utest.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/SimpleString.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestRegistry.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestOutput.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TeamCityTestOutput.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestFilter.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestHarness_c.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestMemoryAllocator.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestResult.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/SimpleMutex.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestFailure.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/CodeMemoryReportFormatter.cpp
  # $ENV{CPPUTEST_HOME}/src/CppUTestExt/IEEE754ExceptionsPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MemoryReportAllocator.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MemoryReportFormatter.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MemoryReporterPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockNamedValue.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockSupport_c.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockExpectedCallsList.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/OrderedTest.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockExpectedCall.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockSupport.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockActualCall.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockFailure.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockSupportPlugin.cpp
  )

SET(CPPUTEST_SOURCES
  ${_CPPUTEST_SOURCES}
  $ENV{CPPUTEST_HOME}/src/Platforms/Iar/UtestPlatform.cpp
  )

SET(INCLUDES_CPPUTEST
  $<BUILD_INTERFACE:$ENV{CPPUTEST_HOME}/include>
  )

STM32F4 では FPU を使うので float 用 テストプラグインはコメントアウトしています。 この CPPUTEST_SOURCES を使用するためにテスト用 executable を追加します。

add_executable(${PROJ}-test
  ...
  )

ここで作成するテストには Release バイナリで使用するファイルを一部共有する必要があります。そのため CMakeLists.txt 内で変数として切り出します。ここまでの変更は以下の diff より確認できます。

modified   CMakeLists.txt
@@ -22,49 +22,61 @@ set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -std=c++11 -fpermissive -fexceptions -fnon
 # set(CMAKE_C_LINK_FLAGS "${CMAKE_C_FLAGS} -T${CMAKE_CURRENT_LIST_DIR}/STM32F411RETx_FLASH.ld  --specs=nano.specs -Wl,--gc-sections")
 set(CMAKE_C_LINK_FLAGS "${CMAKE_C_FLAGS} -T${CMAKE_CURRENT_LIST_DIR}/STM32F411RETx_FLASH.ld  --specs=rdimon.specs -u _scan_float -u _printf_float -Wl,--gc-sections")
 
+set(C_SOURCES_COMMON
+  ${CMAKE_SOURCE_DIR}/Core/Src/stm32f4xx_it.c
+  ${CMAKE_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_gpio.c
+  ${CMAKE_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_rcc.c
+  ${CMAKE_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_utils.c
+  ${CMAKE_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_exti.c
+  ${CMAKE_SOURCE_DIR}/Core/Src/system_stm32f4xx.c
+  )
+
+set(DEFINITIONS_COMMON
+  -DUSE_FULL_LL_DRIVER
+  -DHSE_VALUE=25000000
+  -DHSE_STARTUP_TIMEOUT=100
+  -DLSE_STARTUP_TIMEOUT=5000
+  -DLSE_VALUE=32768
+  -DEXTERNAL_CLOCK_VALUE=12288000
+  -DHSI_VALUE=16000000
+  -DLSI_VALUE=32000
+  -DVDD_VALUE=3300
+  -DPREFETCH_ENABLE=1
+  -DINSTRUCTION_CACHE_ENABLE=1
+  -DDATA_CACHE_ENABLE=1
+  -DSTM32F411xE
+  )
+
 set(C_SOURCES
+  ${C_SOURCES_COMMON}
   ${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/main.c
-  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/stm32f4xx_it.c
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_gpio.c
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_rcc.c
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_utils.c
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_ll_exti.c
-  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/system_stm32f4xx.c
   )
 
 set(ASM_SOURCES
-  ${CMAKE_CURRENT_SOURCE_DIR}/startup_stm32f411xe.s
+  ${CMAKE_SOURCE_DIR}/startup_stm32f411xe.s
   )
 
 set_property(SOURCE
-  ${CMAKE_CURRENT_SOURCE_DIR}/startup_stm32f411xe.s PROPERTY LANGUAGE C)
+  ${CMAKE_SOURCE_DIR}/startup_stm32f411xe.s PROPERTY LANGUAGE C)
 
 add_executable(${PROJ}
   ${C_SOURCES}
   ${ASM_SOURCES}
   )
 
+set(INCLUDES_COMMON
+  ${CMAKE_SOURCE_DIR}/Core/Inc/
+  ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Include/
+  ${CMAKE_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F4xx/Include/
+  ${CMAKE_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Inc/
+  )
+
 target_include_directories(${PROJ} PRIVATE
-  ${CMAKE_CURRENT_SOURCE_DIR}/Core/Inc/
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/CMSIS/Include/
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32F4xx/Include/
-  ${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32F4xx_HAL_Driver/Inc/
+  ${INCLUDES_COMMON}
   )
 
 target_compile_definitions(${PROJ} PRIVATE
-  -DUSE_FULL_LL_DRIVER
-  -DHSE_VALUE=25000000
-  -DHSE_STARTUP_TIMEOUT=100
-  -DLSE_STARTUP_TIMEOUT=5000
-  -DLSE_VALUE=32768
-  -DEXTERNAL_CLOCK_VALUE=12288000
-  -DHSI_VALUE=16000000
-  -DLSI_VALUE=32000
-  -DVDD_VALUE=3300
-  -DPREFETCH_ENABLE=1
-  -DINSTRUCTION_CACHE_ENABLE=1
-  -DDATA_CACHE_ENABLE=1
-  -DSTM32F411xE
+  ${DEFINITIONS_COMMON}
   )
 
 target_link_libraries(${PROJ}
@@ -78,4 +90,4 @@ add_custom_target(${PROJ}.bin ALL
 
 add_dependencies(${PROJ}.bin ${PROJ})
 
-include_directories(test)
+add_subdirectory(test)

test/CMakeLists.text も以下となります。

SET(_CPPUTEST_SOURCES
  $ENV{CPPUTEST_HOME}/src/CppUTest/CommandLineArguments.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/CommandLineTestRunner.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/JUnitTestOutput.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/MemoryLeakDetector.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/MemoryLeakWarningPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestTestingFixture.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/Utest.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/SimpleString.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestRegistry.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestOutput.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TeamCityTestOutput.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestFilter.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestHarness_c.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestMemoryAllocator.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestResult.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/SimpleMutex.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTest/TestFailure.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/CodeMemoryReportFormatter.cpp
  # $ENV{CPPUTEST_HOME}/src/CppUTestExt/IEEE754ExceptionsPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MemoryReportAllocator.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MemoryReportFormatter.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MemoryReporterPlugin.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockNamedValue.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockSupport_c.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockExpectedCallsList.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/OrderedTest.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockExpectedCall.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockSupport.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockActualCall.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockFailure.cpp
  $ENV{CPPUTEST_HOME}/src/CppUTestExt/MockSupportPlugin.cpp
  )
SET(CPPUTEST_SOURCES
  ${_CPPUTEST_SOURCES}
  $ENV{CPPUTEST_HOME}/src/Platforms/Iar/UtestPlatform.cpp
  )
SET(INCLUDES_CPPUTEST
  $<BUILD_INTERFACE:$ENV{CPPUTEST_HOME}/include>
  )
set(TEST_SOURCES
  ${C_SOURCES_COMMON}
  ${CMAKE_CURRENT_SOURCE_DIR}/startup_stm32f411xe.s
  ${CMAKE_CURRENT_SOURCE_DIR}/Src/main.cpp
  )
set(TEST_ASM_SOURCES
  ${CMAKE_SOURCE_DIR}/startup_stm32f411xe.s
  )
set_property(SOURCE
  ${CMAKE_SOURCE_DIR}/startup_stm32f411xe.s PROPERTY LANGUAGE C
  )
add_executable(${PROJ}-test
  ${TEST_ASM_SOURCES}
  ${TEST_SOURCES}
  ${CPPUTEST_SOURCES}
  )
target_include_directories(${PROJ}-test PRIVATE
  ${INCLUDES_COMMON}
  ${INCLUDES_CPPUTEST}
  )
target_compile_definitions(${PROJ}-test PRIVATE
  ${DEFINITIONS_COMMON}
  )
target_link_libraries(${PROJ}-test
  -lm -lnosys
  )
target_link_options(${PROJ}-test
  PRIVATE "-T${CMAKE_SOURCE_DIR}/STM32F411RETx_FLASH.ld"
  PRIVATE "--specs=rdimon.specs"
  PRIVATE "-u _scan_float"
  PRIVATE "-u _printf_float"
  PRIVATE "-Wl,--gc-sections")
add_custom_target(${PROJ}-test.bin ALL
  COMMAND
  ${CMAKE_OBJCPY} -O binary ${CMAKE_CURRENT_BINARY_DIR}/${PROJ}-test ${CMAKE_CURRENT_BINARY_DIR}/${PROJ}-test.bin
  )
add_dependencies(${PROJ}-test.bin ${PROJ}-test)

test/Src/main.cppを以下のように編集します。

#include "CppUTest/CommandLineTestRunner.h"

int main()
{
  int ac = 2;
  const char * av_override[] = { "exec", "-v" };
  return RUN_ALL_TESTS(ac, av_override);
}

ある理由で test/ 以下に startup_stm32f411xe.s をコピーしています。

$ cp ./startup_stm32f411xe.s test/

コードによっては c と c++ の初期化の違いで 以下の呼び出しがエラーの原因となることがあります。

# startup_stm32f411xe.s:95
/* Call static constructors */
    bl __libc_init_array

その場合にコメントアウトして、代わりに main 関数内で同じ処理を行います。

extern "C" {
  static void callConstructors() {
    extern void (*__init_array_start)();
    extern void (*__init_array_end)();
    for (void (**p)() = &__init_array_start; p < &__init_array_end; ++p) {
        (*p)();
    }
  }
}

int main() {
  callConstructors();

  int ac = 2;
  const char * av_override[] = { "exec", "-v" };
  return RUN_ALL_TESTS(ac, av_override);
}

このようになります。ただ、今はまだ必要ないのでこの変更が無くても動作に支障はありません。実行すると以下の結果が得られます。

OK (0 tests, 0 ran, 0 checks, 0 ignored, 0 filtered out, 0 ms)

これでテスト環境が整いました。プロジェクト特有のビジネスロジック・アプリケーションロジックをテストすることができます。

main.c も以下のよ


modified   Core/Src/main.c
@@ -22,7 +22,7 @@
 
 /* Private includes ----------------------------------------------------------*/
 /* USER CODE BEGIN Includes */
-#include <stdio.h>
+#include "app.h"
 /* USER CODE END Includes */
 
 /* Private typedef -----------------------------------------------------------*/
@@ -48,7 +48,6 @@
 /* Private function prototypes -----------------------------------------------*/
 void SystemClock_Config(void);
 /* USER CODE BEGIN PFP */
-extern void initialise_monitor_handles(void);
 /* USER CODE END PFP */
 
 /* Private user code ---------------------------------------------------------*/
@@ -63,7 +62,6 @@ extern void initialise_monitor_handles(void);
 int main(void)
 {
   /* USER CODE BEGIN 1 */
-  initialise_monitor_handles();
   /* USER CODE END 1 */
 
   /* MCU Configuration--------------------------------------------------------*/
@@ -85,6 +83,7 @@ int main(void)
   SystemClock_Config();
 
   /* USER CODE BEGIN SysInit */
+  app_init();
 
   /* USER CODE END SysInit */
 
@@ -98,7 +97,7 @@ int main(void)
   while (1)
   {
     /* USER CODE END WHILE */
-    printf("Hello world\n");
+    app_process();
     /* USER CODE BEGIN 3 */
   }
   /* USER CODE END 3 */

以下に簡単なテスト例を書いておきます。

Core/Inc/app.h

#pragma once

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

  int16_t app_init(void);
  int16_t app_process(void);

#ifdef __cplusplus
}
#endif // __cplusplus
  

Core/Src/app.c

#include "app.h"

#include <stdio.h>

int16_t app_init(void) {
  printf("app initialized\n");

  return 0;
}

int16_t app_process(void) {
  printf("Hello world\n");

  return 0;
}

以上のような app 定義がある場合、test に以下のコードを配置します。

test/Inc/app_test_c.h

#include "CppUTest/TestHarness_c.h"

TEST_GROUP_C_WRAPPER(app_test)
{
  TEST_GROUP_C_SETUP_WRAPPER(app_test);
  TEST_GROUP_C_TEARDOWN_WRAPPER(app_test);
};

TEST_C_WRAPPER(app_test, test_application_flow);

test/Src/app_test_c.c

#include "CppUTest/TestHarness_c.h"

#include "app.h"


TEST_GROUP_C_SETUP(app_test)
{
};

TEST_GROUP_C_TEARDOWN(app_test)
{
};

TEST_C(app_test, test_application_flow)
{
  CHECK_EQUAL_C_INT(0, app_init());
  CHECK_EQUAL_C_INT(0, app_process());
};

test/Src/main.cpp

#include "CppUTest/CommandLineTestRunner.h"

#include <stdio.h>

#include "app_test_c.h"

extern "C" {
  extern void SystemInit(void);
  extern void initialise_monitor_handles(void);
}

int main()
{
  SystemInit();
  initialise_monitor_handles();

  int ac = 2;
  const char * av_override[] = { "exec", "-v" };
  return RUN_ALL_TESTS(ac, av_override);
}

main.cpp main 関数内で SystemInit を行っているので SystemClock などを設定した後にテストが実行されるようにしています。

テスト結果は以下のようにすべてパスとなります。

Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
TEST(app_test, test_application_flow)app initialized
Hello world
- 1 ms

OK (1 tests, 1 ran, 2 checks, 0 ignored, 0 filtered out, 1 ms)

テスト実行までを扱いましたが、組み込み開発では(単体ソフトウェア)テストで確認できるのはレジスタの値、FLASHの読み込み値、関数の返し値など限定的です。ハードウェア特有の機能をすべて対象とすることは不可能だと思います。テスト内容をどう書くかはある程度の経験と割り切りを持つことが必要だと思います。

今回は以上です。