Help us understand the problem. What is going on with this article?

STM32F4 Discovery BoardとCube MXの環境構築、Lチカからprintfポーティングまで

More than 1 year has passed since last update.

この記事について

環境

  • 開発環境
    • Windows 10
    • System Workbench for STM32 (sw4stm32)
    • STM32CubeMX
  • ターゲットマイコン
    • STM32F407 Discovery Board

目的

  • CubeMXでプロジェクトを生成する
  • LEDチカチカ
  • UARTを使えるようにする
  • printfをポーティングする

事前準備

ソフトウェア

  • sw4stm32のインストール (install_sw4stm32_win_64bits-v2.1.exe)
  • cube mxのインストール (en.stm32cubemx.zip)
  • 何らかのターミナルソフトのインストール (Tera Termなど)
  • ドライバはsw4stm32インストール時に一緒にインストールされたような気がするが、もしかしたら別だったかも(すいません、覚えてないです)
    • 必要に応じて、STM32 ST-Link Utilityなどをインストール

ハードウェア

Discovery ボードに搭載されているデバッガ用MCUは、バーチャルCOMポート(VCP)を使用したUART-USB変換機能を提供しますが、デフォルトではターゲットマイコンのSTM32F407とは接続されていません。この接続をしてあげる必要があります。Discoveryボードのユーザマニュアル(DM00039084.pdf)に記載されている通り、下記2本の線を配線してあげます。どうせいつも使うので、ボードの表面で直接つないであげるといいと思います。
image.png

STM32CubeMXでの作業

ハードウェアConfig

  1. CubeMXを開いて、New Projectをクリックする。最初は少し時間がかかると思います。
    image.png

  2. Board Selectorタブに移動して、STM32F4Discoveryボードを選択する
    image.png

  3. Pinoutタブでピン設定をする。とりあえず今回は外部クロックとprintf用UART(USART2)の設定だけする。外部クロック設定はやらなくても動きますが、せっかくボード上に精度のいい8MHzクリスタルが乗っているので、使います。
    image.png

  4. Clock Configurationタブでクロック設定をする。主な設定ポイントは下記の通り。

    1. PLL Source MuxをHSEにする。これは↑で設定した外部クロックを使用する設定です。
    2. System Clock MuxをPLLCLKにする。外部クロック8MHzのままだと遅いのでPLLで逓倍したクロックを使います
    3. PLLと分周器を下記の通り設定して、最大クロック(168MHz)で動くようにします
      1. M = 8
      2. N = 336
      3. P = 2
    4. ペリフェラル(APB)用クロックの分周器を下記の通り設定して、最大クロックで動くようにします
      1. APB1 = /4
      2. APB2 = /2 image.png
  5. ConfigurationタブでUSART2の設定をする

    1. ボーレートを好きな値に設定する。とりあえずこの記事では、Tera Termが対応する最大値 921600 bpsにします。
    2. UARTドライバの実装次第ですが、僕の実装だと受信にDMAを使うことにしたので、RX用DMAを有効にする。転送先メモリはリングバッファとして使用するため、モードはCircularに設定し、メモリアドレスは自動でインクリメントされるようにIncrement Addressにチェックをつける
    3. 他のタブはそのまま (僕の実装だと今回は割り込みは使いませんでした。また、送信も割り込み/DMAは面倒だったので未使用です)

image.png
image.png

プロジェクトの設定

  1. 設定を保存する
    メニューバー -> Project -> Settings で設定する。ここで指定した保存先に、CubeMXの設定ファイル(*.iocファイル)と後でエクスポートするプロジェクトファイル一式が保存される。下記設定でひとまずOK。他のタブはデフォルトのまま。
    image.png

  2. メニューバー -> Project -> Generate Code でCubeMXで設定したプロジェクトファイルのテンプレートがエクスポートされる。自動的にSW4STM32(Eclipse)が起動される。後々、ハードウェア設定を変更する際には、iocファイルを開いて、設定変更後、再度Generate Codeをする。

LEDチカチカ実装 (SW4STM32)

CubeMXでコード生成したプロジェクトを使う際の注意点

自分のコードは必ず下記コメントの中に書く。でないと、再度CubeMXでコード生成したときに消されてしまう。

mycode.c
  /* USER CODE BEGIN 1 */
    My Code comes here
  /* USER CODE END 1 */

LEDチカチカコード

自動生成されたmain.cの中のmain関数にLED制御用コードを追加する。CubeMXで設定したクロック設定やペリフェラル設定を行うコードがあるが、そこはひとまず無視。

main.c
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration----------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART2_UART_Init();

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
      HAL_GPIO_WritePin(GPIOD, LD4_Pin | LD3_Pin, GPIO_PIN_SET);
      HAL_Delay(1000);
      HAL_GPIO_WritePin(GPIOD, LD4_Pin | LD3_Pin, GPIO_PIN_RESET);
      HAL_Delay(1000);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

}

ビルドと実行

  • Ctrl - b またはハンマーアイコンをクリックしてビルド
  • パソコンとDiscoveryボード(USB ミニBの方)をUSBケーブルで接続する
  • 再生ボタンをクリックして実行、または、虫ボタンをクリックしてデバッグ
    • 構成(Run as)を聞かれたら、Ac6STM32 C/C++ Applicationを選択する
  • オレンジと緑のLEDが1秒間隔でチカチカするはず

printf関数のポーティング (SW4STM32)

UARTドライバの実装

ドライバの実装は各々好きにやればいいのですが、一例として下記にコードを挙げます。追加するファイルは、Inc/common.h、Src/uartTerminal/uartTerminal.c、Src/uartTerminal/uartTerminal.hです。printfポーティングのためには、1byteの送受信関数さえあればいいのでドライバ自体はどのように実装しても良いです。ちなみに、この実装はいろいろと手を抜いています。オーバーフローや、タイミング依存の排他処理などを考慮していません。

common.h
#ifndef COMMON_H_
#define COMMON_H_

typedef uint32_t RET;

#define RET_OK           0x00000000
#define RET_NO_DATA      0x00000001
#define RET_DO_NOTHING   0x00000002
#define RET_ERR          0x80000001
#define RET_ERR_OF       0x80000002
#define RET_ERR_TIMEOUT  0x80000004
#define RET_ERR_STATUS   0x80000008
#define RET_ERR_PARAM    0x80000010
#define RET_ERR_FILE     0x80000020
#define RET_ERR_MEMORY   0x80000040

#endif /* COMMON_H_ */
uartTerminal.h
#ifndef UARTTERMINAL_UARTTERMINAL_H_
#define UARTTERMINAL_UARTTERMINAL_H_

RET uartTerminal_init(UART_HandleTypeDef *huart);
RET uartTerminal_send(uint8_t data);
uint8_t uartTerminal_recv();
RET uartTerminal_recvTry(uint8_t *data);

#endif /* UARTTERMINAL_UARTTERMINAL_H_ */
uartTerminal.c
#include <stdio.h>
#include "main.h"
#include "stm32f4xx_hal.h"

#include "common.h"
#include "uartTerminal.h"

/*** Internal Const Values, Macros ***/
#define BUFFER_SIZE 16
#define bufferRxWp ( (BUFFER_SIZE - sp_huart->hdmarx->Instance->NDTR) & (BUFFER_SIZE - 1) )

/*** Static Variables ***/
static UART_HandleTypeDef *sp_huart;
static volatile uint8_t s_bufferRx[BUFFER_SIZE];
static volatile uint8_t s_bufferRxRp = 0;

/*** Internal Function Declarations ***/

/*** External Function Defines ***/
RET uartTerminal_init(UART_HandleTypeDef *huart)
{
  sp_huart = huart;
  HAL_UART_Receive_DMA(sp_huart, s_bufferRx, BUFFER_SIZE);
  s_bufferRxRp = 0;

//  /* echo test */
//  while(1){
//    uartTerminal_send(uartTerminal_recv());
//  }
  return RET_OK;
}

RET uartTerminal_send(uint8_t data)
{
  HAL_StatusTypeDef ret;
  ret = HAL_UART_Transmit(sp_huart, &data, 1, 100);
  if (ret == HAL_OK ) {
    return RET_OK;
  } else {
    return RET_ERR;
  }
}

uint8_t uartTerminal_recv()
{
  uint8_t data = 0;
  while (bufferRxWp == s_bufferRxRp);
  data = s_bufferRx[s_bufferRxRp++];
  s_bufferRxRp &= (BUFFER_SIZE - 1);
  return data;
}

RET uartTerminal_recvTry(uint8_t *data)
{
  if (bufferRxWp == s_bufferRxRp)
    return RET_NO_DATA;
  *data = s_bufferRx[s_bufferRxRp++];
  s_bufferRxRp &= (BUFFER_SIZE - 1);
  return RET_OK;
}

エコーバックのテスト

main.cのループ内を下記のように書き換える。また、作成したヘッダをincludeするようにする

main.c
/* USER CODE BEGIN Includes */
#include "common.h"
#include "uartTerminal/uartTerminal.h"
/* USER CODE END Includes */

- 省略 -

  /* USER CODE BEGIN 2 */
  uartTerminal_init(&huart2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
      char c = uartTerminal_recv();
      uartTerminal_send(c);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

Tera Termなどから、STM32のUARTにVCPで接続する(ドライバは他のソフトウェアをインストールした時に自動でインストールされていたと思うのですが、無かったらググってください。)。

image.png

ボーレートをCubeMXで設定したのと同じ値(この記事の例だと921600)に設定する。適当にキー入力すると、入力した文字が表示されるはずです。

printfのポーティング

syscalls.cの用意

いよいよprintfをポーティングします。printfの仕組みとしては、Cのライブラリがシステムコールを呼び、システムコール内で低レベルなIOアクセス用関数をたたくという感じです。そしてこの低レベルなIOアクセス用関数というのがまさに先ほど作ったUARTドライバになります。そしてその仲立ちをするのがシステムコールになります。その実装は本来であれば、syscalls.cにあるのですが、CubeMXで生成した場合にはプロジェクトに含まれていません。どうやらバグのようです(https://community.st.com/thread/35971-stm32cubemx-and-syscallsc/ )。そのため、手動でsyscalls.cを用意してあげます。

  1. sw4stm32(eclipse)のメニューバーでFile -> New -> C Project -> Ac6 STM32 MCU Project (Toolchains = Ac6 STM32 MCU GCC)

    1. プロジェクト名は適当でいいです。どうせ後で消します。 image.png
  2. MCU Configurationを以下のようにします。これも適当でいいとは思うのですが、一応使用しているボードに合わせます。そして、ここではFinishではなく、Nextをクリック
    image.png

  3. ライブラリとしてCube HALを使用するように設定します。他の設定はどうでもいいです。そしてFinish
    image.png

  4. 新たに作られたプロジェクトのsrcフォルダ内にsyscalls.cがあるので、それを今回のプロジェクトのSrcフォルダにコピーします。
    image.png

一時的に作ったプロジェクトはもう消してしまって大丈夫です。ちなみに、CubeMXを使わないでプロジェクトを作るときはこのような手順になります。

ポーティング

syscalls.cを軽く見ると、_read_write関数の中で__io_getchar__io_putchar関数を呼んでいます。この2関数を実装することがポーティング作業になります。同じファイル内にベタ実装してもいいのですが、今回はたまたまprintfの出力先としてUARTを使いますが、場合によっては液晶ディスプレイや別のデバイスにすることもあると思います。なのでファイルは分けてあげて、後々簡単に切り替えられるようにした方が良いと思います。僕はよくretarget.cという形にしています。retarget.cというファイルをSrc下に新たに作ります。

retarget.c
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include "common.h"
#include "./uartTerminal/uartTerminal.h"

void retarget_init()
{
  extern UART_HandleTypeDef huart2;
  uartTerminal_init(&huart2);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
}


#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

PUTCHAR_PROTOTYPE
{
  uartTerminal_send(ch);
  return 1;
}

#ifdef __GNUC__
#define GETCHAR_PROTOTYPE int __io_getchar(void)
#else
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif /* __GNUC__ */

GETCHAR_PROTOTYPE
{
  return uartTerminal_recv();
}

printfしてみる

下記のようにmain関数を変えてみます。retarget_initを呼ぶのを忘れないようにしてください。printfとgetcharが使えるはずです。ちなみに、scanfも使えます。

main.c main関数の一部
  /* USER CODE BEGIN 2 */
  retarget_init();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
      char c = getchar();
      printf("input is %c\n", c);
      int x = 4;
      printf("%d x 2 = %d\n", x, x * 2);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

浮動小数点(float)の出力をできるようにする。

ただし、このままだと、浮動小数点(float)出力が出来ません。例えば、printf("%lf", 1.2);とかすると、何も表示されません。リンカの設定を下記のように変えてあげる必要があります。こちらを参考にさせていただきました。
image.png

とはいえ、通常の組み込みソフトウェアの実装にも言えることですが、floatはあまり使わない方がいいです。このオプションを有効にするだけでバイナリサイズがだいぶ増えます。

-u _printf_floatオプションなしのバイナリサイズ
text data bss dec hex filename
12408 112 1776 14296 37d8 BasicUart.elf

-u _printf_floatオプション有りのバイナリサイズ
text data bss dec hex filename
21528 476 1772 23776 5ce0 BasicUart.elf

資料など

ソースコード: https://github.com/take-iwiw/STM32_UART_printf

説明ビデオ: https://www.youtube.com/watch?v=i8EkWke46GU

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away