主に自分用メモ
ターゲット
STBee Miniボード(STM32F405RGT)を使用する。
C++でタスクを作る例、USBでPCに接続しCDC(VCP)で通信する例、等。
あちこち自分でコード書かなきゃいけないので、Arduinoとかmbedみたいに簡単に使えるわけではない。自分でコードを書きたくない人は他の環境を使ってください。
基本的には大部分のSTM32で使えるようなテンプレートになってるはず。ただしメモリ使用量は大きめなのでRAMが小さいデバイスだと厳しい。
※ 全体的に手抜き。特にUSB CDC(VCP)周り。
開発環境
Win10/WSL
STM32CubeMX
STM32CubeProgrammer
Visual Studio Code
arm-none-eabi-gcc(Linux版)
Win環境だけどコンパイラはLinux版を使い、bashでmakeを走らせてコンパイルする。
CubeMX
デバイスの選択
File > New Project (Ctrl-N)で使用するチップを選ぶ。
左上のPart Numberに型番を入れると楽。パッケージや機能から選ぶこともできるので、必要なチップを選ぶためのツールとしても使える。
右下にデバイスの一覧が表示される。左端の星マークをクリックするとお気に入りに登録される。左上の星マークをクリックするとお気に入りのデバイスを表示できる。自分がよく使うチップは登録しておくと便利。電子工作レベルで使うなら入手性のいいデバイスは数個程度に絞られる(得体のしれないリマーク品買い漁って運ゲーやってる人なんていないよね?)。
プロジェクトの設定
Project Managerタブからいくつか設定を行う。
Project Nameを適当に設定する。必要に応じてProject Locationも。
今回はMakeベースでビルドするので、ToolchainはMakefileを選ぶ。
MiddlewareからFREERTOSのInterfaceをCMSIS_V2に設定する。
FreeRTOSを使用する際は割り込みの優先度に制約があるので、予めRTOSを使用すると宣言しておかないと、あとで痛い目を見る(という記憶があるので、先に設定する。最近の環境は問題ないのかも)。
RTOSの設定は後でやる。
細々した設定
System CoreからRCCのHigh Speed Clockを設定する。今回は水晶を使うのでCrystalを選択。
System CoreからSYSのTimebase Sourceを適当なタイマに設定する。STM32F4であればTIM6かTIM7あたりがおすすめ(一番協調性がないヤツなので、消極的に選ぶならコレ)。
書き込みにST-Linkを使う場合はDebugにSerial Wireを設定しておく。
ConnectivityからUSB_OTG_FSのModeをDevice_Onlyに設定する。
USB_OTG_HSと間違えないように注意。
Clock Configurationのタブにエラーアイコンが出る(あとでなおす)。
MiddlewareからUSB_DEVICEのClass For FS IPをCommunication Device Class (Virtual Port Com)に設定する。
右側の図から、押しボタンとLEDが接続されているPA0とPD2をそれぞれInputとOutputに設定し、適当に名前をつける。この名前はソースコード内で定義されるので、大量のピンを設定する際は競合しないように気をつけること。
クロックの設定
Clock Configurationタブを開く。
「自動で設定する?」みたいな(英語の)ダイアログが出るので、Noで無視する。
Input frequencyに水晶の値を設定する。
クロックの流れをいい感じに選択する。今回はPLL Source MuxにHSEを選択し、System Clock MuxにPLLCLKを選択した。
最後にFCLK Cortex clockに欲しい周波数を入力し、Enterで確定する。すこし待つと自動で設定され、赤く表示されたエラーも消える。
FreeRTOSの設定
MiddlewareのFREERTOSを開く。
Config parametersのタブからMemory management settingsでヒープサイズを調整する。CubeMXのデフォルトはチップによってまちまちだが、たいてい大きすぎるか小さすぎるか両極端。
大雑把な目安として、タスク1個に最小で512バイト、多い方は使い方に応じて青天井だが、printfやscanfのような重い処理をやる場合は2048から4096バイト程度が必要になる。他にも色々使うので、8192バイト程度があれば、小規模な用途なら足りるかな、という程度。
他は必要に応じて設定する(基本的にデフォルトで問題ない)。
stack/heapのフック
タスクがスタックオーバーフローを起こしたり、mallocで領域が確保できなかった場合に通知する機能がある。これを使うと、不適切なコードを走らせたときに適切に死んでくれるので、「なんか変な動きをしてる」をある程度防ぐことができる(ただしスタックはダイイングメッセージを出す前に死ぬことがあるので、100%発見できるわけではない)。
この機能を使うには、Config parametersタブのUSE_MALLOC_FAILED_HOOL
をEnableに、CHECK_FOR_STACK_OVERFLOW
をOption2に設定し、追加のコードを書く(後述)。
コードの生成
右上のGENERATE CODEをクリックし、コードを生成する。
うまくいけばThe Code is successfully generatedというようなダイアログが出てくる。Open Folderをクリックするとコードが生成されたフォルダを開ける。
以降はテキストエディタの仕事。
ビルド
ソースコードの書き換えとかをやっていく。
Makefile
qiitaはmakefileのタブ情報が失われるので、コピペの際はタブへの修正忘れに注意。
# ASM sources
の行の前に以下を書き加える。
CPP_SOURCES = \
Core/Src/syscalls.cpp \
Core/Src/RTOS_threads/runner.cpp \
Core/Src/RTOS_threads/Console.cpp \
PREFIX = arm-none-eabi-
の下に以下の行を追加する。
GCC_PATH = /mnt/c/Devz/ARM/gcc-arm-none-eabi-10-2020-q4-major/bin
使用するコンパイラの置き場所に合わせて修正すること。
CC = $(GCC_PATH)/$(PREFIX)gcc
の下に以下の行を追加する。
CPP = $(GCC_PATH)/$(PREFIX)g++
LDFLAGSの定義の下に以下の行を追加する。
LDFLAGS += -u_printf_float -u_scanf_float
これによってprintfやscanfの中で浮動小数点数を使えるようになる。不要な場合は追加しなくてもいい(今回はコマンドのサンプルでfloatを使うので設定しておく)。
-specs=nano.specs
の環境では組み込み用のサブセットが使用されるので、フルセットの機能が必要な場合はLDFLAGSの定義から-specs=nano.specs
を削除する(-u_printf_float -u_scanf_float
も不要)。ROMに余裕があるのならnano.specsは削除しておいたほうが安心感がある(「なぜか動かない」みたいな余計なトラブルに巻き込まれる可能性が少し減る)。
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
vpath %.c $(sort $(dir $(C_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
を
# list of objects
OBJECTS = $(addprefix $(BUILD_DIR)/,$(C_SOURCES:.c=.o))
vpath %.c $(sort $(dir $(C_SOURCES)))
OBJECTS += $(addprefix $(BUILD_DIR)/,$(CPP_SOURCES:.cpp=.o))
vpath %.cpp $(sort $(dir $(CPP_SOURCES)))
# list of ASM program objects
OBJECTS += $(addprefix $(BUILD_DIR)/,$(ASM_SOURCES:.s=.o))
vpath %.s $(sort $(dir $(ASM_SOURCES)))
のように書き換える。
(notdirの削除、C++の追加)
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)
$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@
を
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)
mkdir -p $(dir $@);
$(CC) -std=c11 -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(<:.c=.lst) $< -o $@
$(BUILD_DIR)/%.o: %.cpp Makefile | $(BUILD_DIR)
mkdir -p $(dir $@);
$(CPP) -std=c++17 -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(<:.cpp=.lst) $< -o $@
のように書き換える。
(mkdirの追加、c11の明示、notdirの削除、C++の追加)
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
$(SZ) $@
を
$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile
$(CPP) $(OBJECTS) $(LDFLAGS) -o $@
$(SZ) $@
のように書き換える。
(C++でリンクする)
-include $(wildcard $(BUILD_DIR)/*.d)
を以下のように書き換える。
-include $(OBJECTS:%.o=%.d)
設定ファイル (Visual Studio Codeの場合)
ctrl-shift-P
でコマンドパレットを開き、c/c++:editconfigurations (JSON)
を入力し決定(入力は>c/cjson
くらい入れておけば補完してくれる)。設定ファイルの雛形が作成される。
いい感じに設定する。
具体的には、includePathをMakefileのC_INCLUDESから、definesをMakefileのC_DEFSからコピペして書き換える。
以下のようになる。
{
"configurations": [
{
"name": "STM32F405xx",
"includePath": [
"/mnt/c/Devz/ARM/gcc-arm-none-eabi-10-2020-q4-major/arm-none-eabi/include",
"/mnt/c/Devz/ARM/gcc-arm-none-eabi-10-2020-q4-major/arm-none-eabi/include/c++/10.2.1",
"/mnt/c/Devz/ARM/gcc-arm-none-eabi-10-2020-q4-major/arm-none-eabi/include/c++/10.2.1/arm-none-eabi/thumb/v7e-m+fp/hard",
"/mnt/c/Devz/ARM/gcc-arm-none-eabi-10-2020-q4-major/lib/gcc/arm-none-eabi/10.2.1/include",
"${workspaceFolder}/Core/Inc",
"${workspaceFolder}/Drivers/CMSIS/Device/ST/STM32F4xx/Include",
"${workspaceFolder}/Drivers/CMSIS/Include",
"${workspaceFolder}/Drivers/STM32F4xx_HAL_Driver/Inc",
"${workspaceFolder}/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy",
"${workspaceFolder}/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc",
"${workspaceFolder}/Middlewares/ST/STM32_USB_Device_Library/Core/Inc",
"${workspaceFolder}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2",
"${workspaceFolder}/Middlewares/Third_Party/FreeRTOS/Source/include",
"${workspaceFolder}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F",
"${workspaceFolder}/USB_DEVICE/App",
"${workspaceFolder}/USB_DEVICE/Target"
],
"defines": [
"STM32F405xx",
"USE_HAL_DRIVER"
],
"compilerPath": "/mnt/c/Devz/ARM/gcc-arm-none-eabi-10-2020-q4-major/bin/arm-none-eabi-g++",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-arm"
}
],
"version": 4
}
コンパイラのパスや、コンパイラに付属したライブラリのパスは、コンパイラのバージョンや置き場に応じて設定する。
CubeMXで設定を変えるとインクルードや定義が変更される場合があるので、必要に応じて修正する(ビルドが通らないわけではないが、インテリセンスが効かなくなったりするので不便)。
ソースコード
Core/Src/main.c
/* USER CODE BEGIN Includes */
の下に以下の行を追加する。
#include <queue.h>
#include <stm32f4xx.h>
#include <usbd_cdc_if.h>
/* USER CODE BEGIN PV */
の下に以下の行を追加する。
osMessageQueueId_t stdout_queue;
osMessageQueueId_t stdin_queue;
/* USER CODE BEGIN RTOS_QUEUES */
の下に以下の行を追加する。
stdout_queue = osMessageQueueNew(256, 1, 0);
stdin_queue = osMessageQueueNew(256, 1, 0);
/* USER CODE BEGIN RTOS_THREADS */
の下に以下の行を追加する。
{
extern void rtos_tasks_runner(void);
rtos_tasks_runner();
}
void StartDefaultTask(void *argument)
のfor(;;)
の中身を以下のように書き換える。
for (;;)
{
uint8_t buffer[32];
uint16_t count = 0;
BaseType_t dequeue_state = pdPASS;
while (dequeue_state == pdPASS && count < sizeof(buffer))
{
dequeue_state = xQueueReceive(stdout_queue, &buffer[count], 1);
if (dequeue_state)
{
++count;
}
}
if (0 < count)
{
CDC_Transmit_FS(buffer, count);
}
osDelay(2);
}
USB_DEVICE/App/usbd_cdc_if.c
/* USER CODE BEGIN INCLUDE */
の下に以下の行を追加する。
#include <cmsis_os.h>
#include <queue.h>
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length)
のcase CDC_GET_LINE_CODING:
の中に以下の行を追加する。
{
USBD_CDC_LineCodingTypeDef foo = {
.bitrate = 115200,
.format = 0,
.paritytype = 0,
.datatype = 8,
};
*(USBD_CDC_LineCodingTypeDef *)pbuf = foo;
}
PC側からシリアルポートの設定を見たときにダミーの(それらしい)設定を返す。
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
を以下のように書き換える。
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
extern osMessageQId stdin_queue;
uint32_t i = 0;
for (i = 0; i < *Len; i++)
{
xQueueSendFromISR(stdin_queue, &Buf[i], 0);
}
return (USBD_OK);
/* USER CODE END 6 */
}
Core/Inc/FreeRTOSConfig.h
/* USER CODE BEGIN Defines */
の下に以下の行を追加する。
#define configAPPLICATION_ALLOCATED_HEAP 1
Core/Src/RTOS_threads/runner.cpp
Core/Src/RTOS_threads/runner.cpp
を作成し、以下の行を追加する。
#include <cmsis_os2.h>
#include <RTOS_threads/Console.h>
#include <FreeRTOS.h>
#include <stm32f4xx.h>
#include <main.h>
namespace RTOS_threads
{
void LED_blink(void *)
{
for (;;)
{
HAL_GPIO_TogglePin(USER_LED_GPIO_Port, USER_LED_Pin);
osDelay(HAL_GPIO_ReadPin(USER_SW_GPIO_Port, USER_SW_Pin) ? 100 : 500);
}
}
}
extern "C" void rtos_tasks_runner(void)
{
osThreadNew(RTOS_threads::LED_blink, nullptr, nullptr); // デフォルトの優先度・スタックサイズで起動(名前無し)
RTOS_threads::Console::start();
}
#if (configAPPLICATION_ALLOCATED_HEAP == 1)
uint8_t ucHeap[configTOTAL_HEAP_SIZE] __attribute__((section(".ccmram")));
#endif
STM32CubeMXで生成したリンカスクリプトでは、.ccmram
はSTM32F3やF4で定義されている領域で、F1やGx等では定義されていない。とはいえ、定義されていない場合は単に無視されるだけなので、書きっぱなしでも特に実害はない。
Core/Inc/RTOS_threads/Console.h
Core/Inc/RTOS_threads/Console.h
を作成し、以下の行を追加する。
#ifndef RTOS_threads_Console_H
#define RTOS_threads_Console_H
#include <stddef.h>
#include <stdint.h>
namespace RTOS_threads
{
class Console final
{
public:
// Consoleのインスタンスを確保し、RTOSのスレッドを作成する
static bool start(void);
protected:
private:
// cmd_tasklistでスタックに確保するTaskStatus_t配列の大きさ
// すべての実行中のタスクが入る大きさに設定する(should)
static constexpr uint16_t task_status_array_size = 10;
struct Command
{
using Command_function_t = bool (Console::*)(void) const;
Command(const Command_function_t func, const char *const cmd)
: function(func), command(cmd), length()
{
}
Command(const Command_function_t func,
const char *const cmd,
const size_t len)
: function(func), command(cmd), length(len)
{
}
const Command_function_t function;
const char *const command;
const size_t length;
};
static const Command commands[];
// read_lineでstdinから1行ずつ読み込まれる
// それ以外の関数からは書き換え禁止(shall not)
char linebuff[200];
Console(void);
~Console();
Console(const Console &);
Console &operator=(const Console &);
// スレッドのメインループ
void task(void);
// 起動中のスレッドの一覧を表示する
bool cmd_tasklist(void) const;
// RTOSの残りのヒープサイズを表示する
bool cmd_free_heap(void) const;
// 四則演算("add [float] [float]"等)
bool cmd_operations(void) const;
// stdinからlinebuffへ1行分を読み出す
void read_line(void);
// taskの実行、インスタンスの削除、スレッドの削除を行う
// start以外からの呼び出しは禁止(shall not)
static void entry(Console *const ptr);
};
}
#endif
Core/Src/RTOS_threads/Console.cpp
Core/Src/RTOS_threads/Console.cpp
を作成し、以下の行を追加する。
#include <RTOS_threads/Console.h>
#include <cmsis_os2.h>
#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <string.h>
#include <new>
namespace RTOS_threads
{
Console::Command const Console::commands[] = {
Command(&Console::cmd_tasklist, "tasklist"),
Command(&Console::cmd_free_heap, "free heap"),
Command(&Console::cmd_operations, "add [float] [float]", 4),
Command(&Console::cmd_operations, "sub [float] [float]", 4),
Command(&Console::cmd_operations, "mul [float] [float]", 4),
Command(&Console::cmd_operations, "div [float] [float]", 4),
};
bool Console::start(void)
{
auto *const instance = new (std::nothrow) Console();
if (!instance)
{ // インスタンスの確保に失敗した場合は異常終了
return false;
}
const osThreadAttr_t attr = {
"Console",
0,
nullptr,
0,
nullptr,
sizeof(StackType_t) * 512,
osPriorityNormal,
0,
0,
};
if (!osThreadNew(reinterpret_cast<osThreadFunc_t>(entry), instance, &attr))
{ // 無効なスレッドID(ヌルポインタ)が返った場合はインスタンスを破棄して異常終了
delete instance;
return false;
}
// エラー無し:正常終了
return true;
}
Console::Console(void)
: linebuff()
{
}
Console::~Console()
{
}
void Console::task(void)
{
osDelay(1000); // PC接続待ち
printf("hello world\n");
for (;;)
{
read_line();
uint32_t command_executes = 0;
for (const auto &cmd : commands)
{
if (cmd.command &&
(cmd.length == 0
? 0 == strcmp(linebuff, cmd.command)
: 0 == strncmp(linebuff, cmd.command, cmd.length)) &&
cmd.function &&
(this->*cmd.function)())
{
++command_executes;
}
}
if (command_executes)
{ // コマンドの実行に成功
// NOP
}
else if (0 == strcmp(linebuff, "?") ||
0 == strcmp(linebuff, "help"))
{ // コマンド一覧の表示
printf("commands (%d)\n", sizeof(commands) / sizeof(commands[0]));
for (const auto &cmd : commands)
{
printf("\"%s\"\n", cmd.command);
}
printf("\n");
}
else
{ // 入力されたコマンドをエコーバック
printf("unknown command: \"%s\"\n", linebuff);
}
}
}
bool Console::cmd_tasklist(void) const
{
TaskStatus_t task_status_array[task_status_array_size] = {};
const UBaseType_t num_of_tasks = uxTaskGetSystemState(task_status_array, task_status_array_size, nullptr);
printf("Name State Priorty Stack Num\n"
"*******************************************\n");
for (UBaseType_t i = 0; i < num_of_tasks; i++)
{
const TaskStatus_t &task = task_status_array[i];
const char *status = "---";
switch (task.eCurrentState)
{
case eRunning:
status = "Run";
break;
case eReady:
status = "Rdy";
break;
case eBlocked:
status = "Blk";
break;
case eSuspended:
status = "Sus";
break;
case eDeleted:
status = "Del";
break;
case eInvalid:
status = "Inv";
break;
}
printf("%-*s %-5s %-7lu %-5hu %-3lu\n",
configMAX_TASK_NAME_LEN, task.pcTaskName,
status,
task.uxCurrentPriority,
task.usStackHighWaterMark,
task.xTaskNumber);
}
printf("*******************************************\n");
return true;
}
bool Console::cmd_free_heap(void) const
{
// -space=nano.spacesな環境では%zuが使えないので、整数にキャストして表示する
printf("free heap: %lu (min: %lu)\n",
static_cast<uint32_t>(xPortGetFreeHeapSize()),
static_cast<uint32_t>(xPortGetMinimumEverFreeHeapSize()));
static_assert(sizeof(size_t) == sizeof(uint32_t));
// %zuが使える場合は以下のようにしてもいい
// printf("free heap: %zu (min: %zu)\n", xPortGetFreeHeapSize(), xPortGetMinimumEverFreeHeapSize());
// ただし%zuが使えない環境でビルドしてもエラーや警告は出ず、実行時に無視される
return true;
}
bool Console::cmd_operations(void) const
{
float op1 = 0, // オペランド1
op2 = 0, // オペランド2
acc = 0; // 計算結果
bool status = false; // 戻り値
status = true; // 先にフラグをセットしておき、else ifで探索、すべてが失敗した場合は最後のelseでクリアする
if (false)
{
// NOP
}
else if (2 == sscanf(linebuff, "add %f %f", &op1, &op2))
{
acc = op1 + op2;
}
else if (2 == sscanf(linebuff, "sub %f %f", &op1, &op2))
{
acc = op1 - op2;
}
else if (2 == sscanf(linebuff, "mul %f %f", &op1, &op2))
{
acc = op1 * op2;
}
else if (2 == sscanf(linebuff, "div %f %f", &op1, &op2))
{
acc = op1 / op2;
}
else
{
status = false;
}
if (status)
{
printf("result: %f\n", acc);
}
return status;
}
void Console::read_line(void)
{
static_assert(200 == sizeof(linebuff));
scanf("%199[^\n]%*[^\n]", linebuff);
getchar();
// 'CR'がある場合は削除する
char *const p = strchr(linebuff, '\r');
if (p)
{
*p = '\0';
}
}
void Console::entry(Console *const ptr)
{
if (ptr)
{
ptr->task(); // タスクを実行
delete ptr; // タスクが終了した場合はインスタンスを削除
}
osThreadExit(); // スレッドを削除して終了
}
}
Core/Src/syscalls.cpp
Core/Src/syscalls.cpp
を作成し、以下の行を追加する。
#include <stm32f4xx.h>
#include <errno.h>
#include <sys/stat.h>
#include <cmsis_os.h>
#include <cmsis_gcc.h>
#include <queue.h>
extern "C"
{
int _close(int)
{
return 0;
}
void _exit(int)
{
label:
goto label;
}
int _fstat(int, struct stat *const st)
{
st->st_mode = S_IFCHR;
return 0;
}
int _getpid(int)
{
return 1;
}
int _isatty(int)
{
return 1;
}
int _kill(int, int)
{
errno = EINVAL;
return -1;
}
_off_t _lseek(int,
_off_t,
int)
{
return 0;
}
_ssize_t _read(const int file,
void *const ptr,
const size_t len)
{
extern osMessageQId stdin_queue;
const QueueHandle_t queue = static_cast<QueueHandle_t>(stdin_queue);
uint8_t *const p = static_cast<uint8_t *>(ptr);
size_t i = 0;
uint8_t ch = 0;
for (i = 0; i < len && ch != '\n'; ++i)
{
xQueueReceive(queue, &ch, portMAX_DELAY);
p[i] = ch;
}
return static_cast<_ssize_t>(i);
}
void *_sbrk(const ptrdiff_t nbytes)
{
vPortEnterCritical();
static char *heap_ptr = 0;
if (!heap_ptr)
{
extern char end[];
heap_ptr = end;
}
char *result = 0;
extern char _estack[];
extern char _Min_Stack_Size[];
if (heap_ptr + nbytes <= reinterpret_cast<char *>(_estack - _Min_Stack_Size - 4))
{
result = heap_ptr;
heap_ptr += nbytes;
}
else
{
result = reinterpret_cast<char *>(-1);
errno = ENOMEM;
}
vPortExitCritical();
return result;
}
_ssize_t _write(const int file,
const void *const ptr,
const size_t len)
{
extern osMessageQId stdout_queue;
const QueueHandle_t queue = static_cast<QueueHandle_t>(stdout_queue);
const uint8_t *const p = static_cast<const uint8_t *>(ptr);
size_t i = 0;
if (__get_IPSR() == 0)
{ // call from thread mode
for (i = 0; i < len; ++i)
{
xQueueSend(queue, &p[i], portMAX_DELAY);
}
}
else
{ // call from interrupt service routine
for (i = 0; i < len; ++i)
{
xQueueSendFromISR(queue, &p[i], 0);
}
}
return static_cast<_ssize_t>(i);
}
}
Core/Src/freertos.c
stack/heapのフックを使用する場合に必要となるコード。
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
の中に以下のコードを追加する。
__disable_irq();
while (1)
{
HAL_GPIO_WritePin(USER_LED_GPIO_Port, USER_LED_Pin, GPIO_PIN_SET);
for (volatile uint32_t i = 0; i < 200000; i++)
{
}
HAL_GPIO_WritePin(USER_LED_GPIO_Port, USER_LED_Pin, GPIO_PIN_RESET);
for (volatile uint32_t i = 0; i < 500000; i++)
{
}
}
void vApplicationMallocFailedHook(void)
の中に以下のコードを追加する。
__disable_irq();
while (1)
{
HAL_GPIO_WritePin(USER_LED_GPIO_Port, USER_LED_Pin, GPIO_PIN_SET);
for (volatile uint32_t i = 0; i < 1000000; i++)
{
}
HAL_GPIO_WritePin(USER_LED_GPIO_Port, USER_LED_Pin, GPIO_PIN_RESET);
for (volatile uint32_t i = 0; i < 10000000; i++)
{
}
}
forループのカウント値は適宜調整すること(この値は168MHz向け)。
スタックオーバーフローは早い点滅、メモリ確保失敗の場合は遅い点滅で通知する。
ビルド
Ctrl-Shift-@
でターミナル(WSL)を開く。
makeでビルドする。
書き込み
STM32CubeProgrammerがインストールされていればGUIやCLIが使える(好みで選択)。個人的にはいちいちウインドウを切り替えなくて済む(VS Codeで完結できる)のでCLIが好き。
SWDで書き込む
"/mnt/c/Program Files/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI.exe" -c port=SWD -d build/*.hex -s
配線が長いなどの理由でエラーになる場合は周波数を下げて接続する。
"/mnt/c/Program Files/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI.exe" -c port=SWD freq=400 -d build/*.hex -s
SWDで書き込む場合、ボードのリセット操作等が不要になる(コマンド1個で接続・書き込み・再起動が行える)。
システムメモリブートモードで書き込む
BOOT0をHに、BOOT1(存在する場合)をLにしてリセットし、以下のコマンドで書き込む(USBの例)。
"/mnt/c/Program Files/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI.exe" -c port=usb1 -d build/*.hex
他のインターフェース(UART等)でも同様に書き込めるはず(詳細は--help
を参照)。
システムメモリブートモードを使う場合、かなりの数のピンが低インピーダンスやプルアップ・プルダウンに設定される。外部との接続に影響があったり、あるいは外部からの入力(GPSモジュールとか)で誤ったブートローダが起動する可能性があるので、システムブートローダを使う可能性がある場合はピンアサインを注意深く設定すること(詳細はAN2606を参照)。
実行
意図したとおりに動いた場合、LEDが1Hz50%で点滅し、PCにUSBが認識されてSTMicroelectronics Virtual COM Port
というデバイスが認識される。適当なターミナルソフト(Tera Term等)で接続し、tasklist
と入力した上でEnterを押すと、RTOSに登録されているタスクの一覧が表示される。ターミナルの設定は、ボーレートやパリティ等の設定は無視される(仮想COMポートのデータ転送だけを使用しているので)。ただし改行コードはLF
またはCRLF
を設定する必要がある。
押しボタンを押すと1Hzの点滅が5Hzへ変化する。
個人的によく落ちる落とし穴
DMAの初期化の順序
DMAを併用するペリフェラルを初期化する際に、CubeMXがペリフェラルを初期化してからDMAを初期化するコードを生成することがある(例えば'MX_LPUART1_UART_Init'を呼んでから'MX_DMA_Init'を呼ぶなど)。DMAモジュールの有効化はMX_DMA_Initの中で行われ、それ以前にDMAの設定を行っても、その設定は破棄されてしまう。そのため、MX_DMA_Initは先に呼んでおく必要がある。これはUSER CODE
で囲われていないので、CubeMXでコードを生成するたびに修正する必要がある。あるいは、USER CODE BEGIN SysInit
内でDMAの有効化を行っておくという手もある。
SPIやUARTなどを使う際にDMAでハマったときはmain.cを確認すると良いかも。
DACのDMA転送
DACのレジスタはワード単位(32bit)でアクセスする必要がある。
CubeMXでDACのDMAを設定すると、デフォルトでハーフワード(16bit)でアクセスするように設定される。
その他
インスタンス/スタック
今回のサンプルでは、LED_blinkとConsoleの2つのスレッドを作成した。LED_blinkはRTOSが管理するメモリにスタックを置いて関数を呼び出した。Consoleのインスタンスは標準ライブラリが管理するメモリに置き、スタックはRTOSが管理するメモリに置いて起動した。
今回のサンプルではRTOSが管理する領域はCCMRAMに配置したため、STM32F4等ではスタックに置いたバッファをDMA転送に使うことはできない。DMA転送を行いたいデータはインスタンスにバッファを確保し、標準ライブラリが管理する通常のメモリエリアに配置する必要がある。
アイドル
FREERTOSのConfig parametersでUSE_IDLE_HOOKをEnableに設定すると、RTOSがアイドル状態の時にCore/Src/freertos.c
のvoid vApplicationIdleHook(void)
がコールされる。例えばこの中で__WFI()を呼べば、無負荷のときにコアをスリープさせることができる(もっとも、これによる消費電力削減効果は限定的なので、電力に厳しい用途の場合はクロック数を下げるほうが効果的だが)。
アイドル時間の推定
適当な場所(main.cの/* USER CODE BEGIN 2 */の下など)でタイマをPWMモードで初期化し、IdleHookの中でタイマにUGイベントを送ると、アイドル時間をある程度推定できるようになる。
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_TIM9_CLK_ENABLE();
GPIO_InitTypeDef gpio_init = {
.Pin = GPIO_PIN_2,
.Mode = GPIO_MODE_AF_PP,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_HIGH,
.Alternate = GPIO_AF3_TIM9,
};
HAL_GPIO_Init(GPIOA, &gpio_init);
TIM_HandleTypeDef htim = {
.Instance = TIM9,
.Init = {
.Prescaler = 30, // 個別に調整
.CounterMode = TIM_COUNTERMODE_UP,
.Period = 0xFFFF,
},
};
HAL_TIM_PWM_Init(&htim);
TIM_OC_InitTypeDef oc_init = {
.OCMode = TIM_OCMODE_PWM1,
.Pulse = 1,
};
HAL_TIM_PWM_ConfigChannel(&htim, &oc_init, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_1);
}
void vApplicationIdleHook(void)
{
TIM9->EGR = TIM_EGR_UG;
}
これにより、十分な頻度でIdleHookが呼ばれている際はタイマのカウンタが0に維持され、PWMのHighが出続ける。割り込みやRTOSのタスクによってIdleHookの呼び出しが中断するとカウンタがインクリメントされ始め、PWMのLowが出力される。このピンをロジック・アナライザやオシロスコープで監視することでRTOSのアイドル率をある程度推定できる。
オレンジの線がタイマのPWM出力で、Highの時はアイドル状態、Lowの時はビジー状態、という表示。
この画像の場合、1kHzのRTOSのコンテキストスイッチとは別に1kHzの割り込み処理が発生していて、時々数百usくらいの処理が発生し、それ以外のほとんどはRTOSがアイドル状態である、というふうな判断ができる。
もっと細かい情報(走っているタスクのID等)が見たい場合は別の方法を使う必要があるが、大雑把にコアの使用率を確認するならこの程度でも十分。
STBee/STBeeMiniの場合
STM32F1を載せたSTBeeボードは、Flashの先頭12KiBにUSBブートローダが書き込まれている。これに対応する方法。
リンカスクリプト
STM32F103VETx_FLASH.ld
のFLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
をFLASH (rx) : ORIGIN = 0x8003000, LENGTH = 512K-12K
のように書き換える(STBeeの場合)。
リンカスクリプトはCubeMXでコードを生成し直すたびに書き換えられる可能性があるので注意すること。誤って0x8000000のままでビルドし書き込みを行うとUSBブートローダを破壊する危険性がある(可能であればあらかじめUSBブートローダを吸い出してPCにバックアップしておいたほうが安全)。
割り込みベクタ
割り込みベクタの位置も変わるので、SCB->VTOR = FLASH_BASE | 0x3000;
のように指定する。
どこに書いてもいいけど、とりあえずmainの一番最初(USER CODE BEGIN1の下)あたりに書いておけば間違いないかと。
USBの有効化
STBee系ボードはUSB有効化を手動で行う必要がある。CubeMXでUSB_ENABLE
ピンを適切な論理で定義しておき、適当な場所でHAL_GPIO_WritePin(USB_ENABLE_GPIO_Port, USB_ENABLE_Pin, GPIO_PIN_RESET); // PD3(STBee)
のように有効化を行う。StartDefaultTask
内で無限ループに入る前に呼んでおけばいいかと。
STBeeとSTBeeMiniは、スイッチやUSB有効信号など、いくつかのピンの論理が逆なので注意すること。
雑記
以前はヘッダファイルでregisterを使用するマクロが定義されており、C++17でコンパイルすると警告が出ていた。いつのまにかこの定義が消えてC++17でコンパイルしても問題ないようになっている。
現在の最新はC++20だが、volatileの複合代入演算子に警告が出るようになったため、特に組み込み用途では大量の警告が出るようになった。pragmaで警告を消すこともできるが、無闇に警告を消すのも考えものなので、とりあえず今回はC++17を使っている。