STM32CubeMX:RTOSでLチカしてみる(v4.26.1)

## 追記とか

STM32CubeMX v4.27.0になってから、makefileの書き出しに失敗する不具合は解消したようです。

## ターゲット

　STBee F4miniでLチカします。
　ついでにUSB CDC(仮想COMポート)でハローワールドもやります。

　以前に書いたエントリと似たような内容ですが、CubeMXのバージョンが変わって方法も変わったので書き直します。

## CubeMX

### プロジェクトの作成

　New Projectをクリックし、New Project画面を出します。
　左上のPart Number Searchで使用するMCUの型番を入力し、右下に表示されたリストをダブルクリックで決定します。

![2018-08-02_21-11.png](https://qiita-image-store.s3.amazonaws.com/0/264452/81ed4067-b452-1af9-8945-d272e3322342.png)

### プロジェクトの設定

　Project → Settings...でProject Settings画面を表示します。

　Project NameとProject Locationを任意に設定します。
　Locationで選択したフォルダに名前がProject Nameのフォルダが作成され、その中に各種ファイルが作成されます。

　Toolchain / IDEでMakefileを選びます。

***

　Code Generatorタブを開きます。

　Generated filesでGenerate peripheral initialization as a pair or '.c/.h' files per peripheralにチェックを入れます。
　Keep...とDelete...にもチェックが入っていることを確認します。

　HAL SettingsでSet all free pins as analog (to optimize the power consumption)とEnable Full Assertにチェックを入れます。

　OkでProject Settingsを閉じます。

### ペリフェラルの設定

　左側のPeripheralsツリーから設定します。


　RCCを開き、High Speed Clock (HSE)でCrystal/Ceramic Resonatorを選択します。


　USB_OTG_FSを開き、ModeでDevice_Onlyを選択します。


　右のMCUのイラストから、PA0をクリックし、GPIO_Inputを選択します。また右クリックし、Enter User Labelをクリックし、`USER_SW`と入力します。
　同じ手順で、PD2をGPIO_Outputにし、`USER_LED`と設定します。

### クロックの設定

　Clock Configurationタブを開きます。
　Clock configurationでDo you want to run automatic clock issues solver ? というダイアログが出てきますが、Noで閉じます。

　左のInput frequencyに12と入力します。
　PLL Source MuxでHSEを選択します。System Clock MuxでPLLCLKを選択します。
　右のHCLK to AHB bus, core, memory and DMA (MHz)に168と入力し、Enterで決定します。

### その他の設定

　Configurationタブを開きます。

　左のツリーから、FREERTOSのEnableにチェックを入れます。

　USB_DEVICEのClass For FS IPでCommunication Device Class (Virtual Port Com)を選択します。

***

　右のMiddlewaresから、FREERTOSのボタンをクリックし、FREERTOS Configurationダイアログを開きます。
　Config parametersタブを開きます。

　Memory AllocationをDynamic / Staticにします。

　USE_IDLE_HOOKとUSE_TRACE_FACILITYとUSE_STATS_FORMATTING_FUNCTIONSをEnableにします。

***

　Tasks and Queueｓタブを開きます。

　LEDを点滅させるタスクを作成します。
　Tasksの欄でAddをクリックし、New Taskダイアログを開きます。
　Task Nameに`LED_blink`、PriorityをosPriorityLowに、Stack Sizeはデフォルトのまま、Entry Functionを`LED_blink_func`に、Code Generation OptionにAs externalに、AllocationにStaticを設定します。

　同様にコンソール機能のタスクも作成します。
　Nameに`Console`、PriorityにosPriorityBelowNormal、Stack Sizeに1024に、Enter Functionに`Console_func`、Code Generation OptionにAs esternal、AllocationにStaticを設定します。

***

　次にキューを作成します。
　今回は標準入力(STDIN)と標準出力(STDOUT)にキューを使用します。

　Queuesの欄でAddをクリックし、New Queueダイアログを開きます。
　Queue Nameに`stdout_queue`、Queue Sizeに256、Item Sizeにuint8_t、AllocationにStaticを設定します。

　同じように、`stdin_queue`、128、uint8_t、Staticで作成します。


　OkでFREERTOS Configurationダイアログを閉じます。

### コードの生成

　Project → Generate Code、またはCtrl-Shift-Gでコードを生成します。

　WARNING: - When FreeRTOS is used, it is strongly recommended to use a HAL timebase source other than the Systick. The HAL timebase source can be changed from the Pinout tab under SYSというダイアログが表示されますが、Yesで続行します。
　(自分の用途では問題が発生していないので、無視します。たぶんFreeRTOSのコンテキストスイッチを1kHz以外にしたり、割り込みの中でタイムアウト系のHAL APIを使用すると問題が起きるはずです)

　しばらく待つとThe Code is successfully generatedと表示されます。
　最近のCubeでは、すでにMakefileがある場合、Makefileの生成に失敗する場合があります。その点覚えておいてください。

## VS Code

　Visual Studio Codeで生成したフォルダを開きます。

### Makefile

　まずはMakefileを修正します。

　`C_SOURCES`の定義の下に以下のコードを追加します。

```makefile
C_SOURCES += \
Src/syscalls.c \

CPP_SOURCES = \
Src/FreeRTOS_tasks/LED_blink.cpp \
Src/FreeRTOS_tasks/Console.cpp \
```

　`PREFIX`の定義の下に以下のコードを追加します。

```makefile
GCC_PATH = /mnt/c/Devz/ARM/launchpad/bin
```

　`CC`、`AS`、`CP`、`SZ`のファイル名の最後に.exeを追加します。

　`CC`の定義の下に以下のコードを追加します。

```makefile
CPP = $(GCC_PATH)/$(PREFIX)g++.exe
```

 `LDFLAGS`の定義から`-specs=nano.specs`を削除します。

　`OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))`を`OBJECTS = $(addprefix $(BUILD_DIR)/,$(C_SOURCES:.c=.o))`に
　`OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o)))`を`OBJECTS += $(addprefix $(BUILD_DIR)/,$(ASM_SOURCES:.s=.o))`に
　`$(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(<:.c=.lst)) $< -o $@`を`$(CC) -std=c11 -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(<:.c=.lst) $< -o $@`に、それぞれ変更します。

　`# list of ASM program objects`の上に以下のコードを追加します。

```makefile
OBJECTS += $(addprefix $(BUILD_DIR)/,$(CPP_SOURCES:.cpp=.o))
vpath %.cpp $(sort $(dir $(CPP_SOURCES)))
```

　`$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR)`の下に`mkdir -p $(dir $@);`を追加します。

　`$(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR)`の上に以下のコードを追加します。

```makefile
$(BUILD_DIR)/%.o: %.cpp Makefile | $(BUILD_DIR)
	mkdir -p $(dir $@);
	$(CPP) -std=c++14 -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(<:.cpp=.lst) $< -o $@
```

　`$(CC) $(OBJECTS) $(LDFLAGS) -o $@`を`$(CPP) $(OBJECTS) $(LDFLAGS) -o $@`に変更します。

　`-include $(wildcard $(BUILD_DIR)/*.d)`を削除し、以下のコードを追加します。

```makefile
DEPS = $(OBJECTS:%.o=%.d)
-include $(DEPS)
```

### 設定ファイル

　VS Codeの設定ファイルを作成します。

　まず、適当なソースファイルを開いておきます。なんでも良いですが、とりあえず`Src/main.c`でも開きましょう。
　そしてCtrl-Shift-Pでコマンドパレットを開き、`c/cpp:editconfigurations`と入力して`c_cpp_properties.json`を作成します。

　`includePath`と`defines`にMakefileのパラメータをセットします。最終的に以下のようになるはずです(一部抜粋)。

```json
"includePath": [
    "${workspaceFolder}/Inc",
    "${workspaceFolder}/Drivers/STM32F4xx_HAL_Driver/Inc",
    "${workspaceFolder}/Drivers/STM32F4xx_HAL_Driver/Inc/Legacy",
    "${workspaceFolder}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_CM4F",
    "${workspaceFolder}/Middlewares/ST/STM32_USB_Device_Library/Core/Inc",
    "${workspaceFolder}/Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc",
    "${workspaceFolder}/Drivers/CMSIS/Device/ST/STM32F4xx/Include",
    "${workspaceFolder}/Middlewares/Third_Party/FreeRTOS/Source/include",
    "${workspaceFolder}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS",
    "${workspaceFolder}/Drivers/CMSIS/Include"
],
"defines": [
    "USE_HAL_DRIVER",
    "STM32F405xx"
],
"compilerPath": "/mnt/c/Devz/ARM/launchpad/bin/arm-none-eabi-gcc.exe",
"cStandard": "c11",
"cppStandard": "c++14",
```

### ソースコード

　いよいよソースコードの編集に入ります。

　`Src/freertos.c`を開きます。
　`/* USER CODE BEGIN Includes */`の下に以下のコードを追加します。

```c
#include <stm32f4xx.h>
#include <usbd_cdc_if.h>
```

　`vApplicationIdleHook`関数の中に以下のコードを追加します。

```c
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
```

　`StartDefaultTask`関数の中の無限ループ(`for(;;)`)の中を以下のように変更します。

```c
static uint8_t buffer[32];
uint16_t count = 0;
BaseType_t dequeue_state = pdPASS;

while (dequeue_state == pdPASS && count < sizeof(buffer))
{
    dequeue_state = xQueueReceive(stdout_queueHandle, &buffer[count], 1);

    if (dequeue_state)
    {
        count++;
    }
}

if (count > 0)
{
    CDC_Transmit_FS(buffer, count);
}

osDelay(2);
```

***

　`Src/usbd_cdc_if.c`を開きます。

　`/* USER CODE BEGIN INCLUDE */`の下に以下のコードを追加します。

```c
#include <cmsis_os.h>
```

　`CDC_Control_FS`関数の中の、switchの中の`CDC_GET_LINE_CODING`に以下のコードを追加します。

```c
*((uint32_t *)&pbuf[0]) = 115200;
pbuf[4] = 1;
pbuf[5] = 0;
pbuf[6] = 8;
```

　`CDC_Receive_FS`関数の中の、returnの前に以下のコードを追加します。

```c
extern osMessageQId stdin_queueHandle;
uint32_t i = 0;
for (i = 0; i < *Len; i++)
{
    xQueueSendFromISR(stdin_queueHandle, &Buf[i], 0);
}
```

***

　`Src/FreeRTOS_tasks/LED_blink.cpp`を作成し、以下のコードを追加します。

```c++
#include <cmsis_os.h>
#include <main.h>
#include <stm32f4xx_hal.h>

extern "C" void LED_blink_func(void const *argument)
{
    while (1)
    {
        HAL_GPIO_TogglePin(USER_LED_GPIO_Port, USER_LED_Pin);
        osDelay(HAL_GPIO_ReadPin(USER_SW_GPIO_Port, USER_SW_Pin) == GPIO_PIN_SET
                    ? 100
                    : 500);
    }
}
```

***

　`Src/FreeRTOS_tasks/Console.cpp`を作成し、以下のコードを追加します。

```c++
#include <cmsis_os.h>
#include <stm32f4xx_hal.h>
#include <string.h>
#include <math.h>

extern "C" void Console_func(void const *argument)
{
    osDelay(500);
    printf("hello world\n");
    printf("sqrt(2):%f\n", sqrtf(2));

    while (1)
    {
        char str[100];
        scanf("%99[^\n]%*[^\n]", str);
        getchar();

        {
            char *const p(strchr(str, '\r'));

            if (p)
            {
                *p = '\0';
            }
        }

        if (!strcmp(str, "tasklist"))
        {
            char buff[10 * 45];
            osThreadList((uint8_t *)buff);
            printf(
                "Name            State   Priorty Stack   Num\n"
                "*******************************************\n"
                "%s"
                "*******************************************\n",
                buff);
        }
    }
}
```

***

　`Src/syscalls.c`を作成し、以下のコードを追加します。

```c
#include <errno.h>
#include <sys/stat.h>
#include <cmsis_os.h>
#include <stm32f4xx_hal.h>

int _close_r(struct _reent *r, int file)
{
    return 0;
}

void _exit(int n)
{
label:
    goto label;
}

int _fstat_r(struct _reent *r,
             int file,
             struct stat *st)
{
    st->st_mode = S_IFCHR;
    return (0);
}

int _getpid(int file)
{
    return 1;
}

int _isatty_r(struct _reent *r, int fd)
{
    return (1);
}

int _kill(int pid, int sig)
{
    errno = EINVAL;
    return -1;
}

_off_t _lseek_r(struct _reent *r,
                int file,
                _off_t ptr,
                int dir)
{
    return ((_off_t)0);
}

_ssize_t _read_r(struct _reent *r,
                 int file,
                 void *ptr,
                 size_t len)
{
    int i;
    uint8_t *p = (uint8_t *)ptr;
    extern osMessageQId stdin_queueHandle;

    for (i = 0; i < len; i++)
    { // break exit

        uint8_t data;
        xQueueReceive(stdin_queueHandle, &data, portMAX_DELAY);

        p[i] = data;

        if (data == '\n')
        {
            i++;
            break;
        }
    }

    return (i);
}

void *_sbrk_r(struct _reent *_s_r, ptrdiff_t nbytes)
{
    static char *heap_ptr = 0;

    if (!heap_ptr)
    {
        extern char end[];
        heap_ptr = end;
    }

    extern char _estack[];
    extern char _Min_Stack_Size[];
    if (heap_ptr + nbytes > (char *)(_estack - _Min_Stack_Size - 4))
    {
        errno = ENOMEM;
        return ((caddr_t)-1);
    }

    char *base = heap_ptr;
    heap_ptr += nbytes;

    return (base);
}

_ssize_t _write_r(struct _reent *r,
                  int file,
                  const void *ptr,
                  size_t len)
{
    int i;
    uint8_t *p = (uint8_t *)ptr;
    extern osMessageQId stdout_queueHandle;

    if ((__get_IPSR() & 0x000001FF) == 0)
    { // interrupt program status register is base level

        for (i = 0; i < len; ++i)
        {
            xQueueSend(stdout_queueHandle, &p[i], portMAX_DELAY);
        }
    }
    else
    { // call from interrupt service routine

        for (i = 0; i < len; ++i)
        {
            xQueueSendFromISR(stdout_queueHandle, &p[i], 0);
        }
    }

    return (i);
}
```

## ビルド&ライト&ラン

　bashでmakeすれば./buildに.binや.hexが生成されます。
　ツールに応じて選択し、マイコンに書き込みます。

　実行すればPCにCOMポートが認識されるはずです。
　このポートにTera Term等で接続します。ボーレートやビット数、パリティ等は変更しなくても大丈夫です。改行コードは受信がAUTO、送信がCR+LFです。
　マイコンをリセットするたびに切断されるので、自動で再接続してくれるターミナルソフトを使ったほうが楽です。

　マイコンをリセットすると、一旦COMポートが切断され、再び接続されて、ターミナルに

```
hello world
sqrt(2):1.414214
```

　と表示されるはずです。

　また`tasklist`とタイプし改行すれば

```
Name            State   Priorty Stack   Num
*******************************************
Console         R       2       616     3
IDLE            R       0       104     4
LED_blink       B       1       104     2
defaultTask     B       3       86      1
*******************************************
```

　のように表示されます。
　この場合、`Console`、`IDLE`、`LED_blink`、`defaultTask`の4個のタスクが有ることがわかります。

　スタックがゼロになるといわゆるスタックオーバーフローが発生し、正常に動作しなくなります。PCのプログラムと違い、意味不明な再現性の有る動作に入る場合が多いので注意してください。なにか法則性がありそうだ、と、いろいろいじった挙げ句、スタックオーバーフローだった、という事が多いです。MCUのRAMと相談しながら、最初は可能な限り大きなスタックを割り当てておいて、アルゴリズムが安定してきたら必要に応じてスタックを減らす、という流れが確実です。
　printfのような多機能な関数を呼ぶとスタックがガッツリと減ったりします。最初は多めに確保しておくべきです。経験則的に、だいたい1024を指定しておけば大抵は足りますが、128では全く足りません。
　今回はConsoleでは1024ワードを、LED_blinkでは128ワードを指定しました。
　LED_blinkは非常に簡単な処理なので、24ワードしか使用していません。だいぶ余裕がありますが、128が最小の推奨値なので、これを下限とします。
　Consoleも408ワードを使っているのみで、だいぶ余裕がありますが、このタスクは後で機能を追加した際にスタックをより多く使用する処理が増える可能性があるので、とりあえず1024のままにしておきます。

## その他

　今回は言語バージョンをCはc11、C++はC++14を指定しました。C++の最新バージョンはC++17ですが、C++17ではregister指定子が削除されており、使用されているとエラーになってしまいます。
　一部のヘッダファイル、インライン関数の中でregisterを使っている部分があり、これがエラーにならないように今回はC++14を指定しました。


## 更新履歴

* 2018/08/07
  * makefileの部分を修正