#この記事について
環境
- 開発環境
- 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本の線を配線してあげます。どうせいつも使うので、ボードの表面で直接つないであげるといいと思います。
STM32CubeMXでの作業
ハードウェアConfig
-
Pinoutタブでピン設定をする。とりあえず今回は外部クロックとprintf用UART(USART2)の設定だけする。外部クロック設定はやらなくても動きますが、せっかくボード上に精度のいい8MHzクリスタルが乗っているので、使います。
-
Clock Configurationタブでクロック設定をする。主な設定ポイントは下記の通り。
5. PLL Source MuxをHSEにする。これは↑で設定した外部クロックを使用する設定です。
6. System Clock MuxをPLLCLKにする。外部クロック8MHzのままだと遅いのでPLLで逓倍したクロックを使います
7. PLLと分周器を下記の通り設定して、最大クロック(168MHz)で動くようにします
8. M = 8
9. N = 336
10. P = 2
11. ペリフェラル(APB)用クロックの分周器を下記の通り設定して、最大クロックで動くようにします
12. APB1 = /4
13. APB2 = /2
-
ConfigurationタブでUSART2の設定をする
6. ボーレートを好きな値に設定する。とりあえずこの記事では、Tera Termが対応する最大値 921600 bpsにします。
7. UARTドライバの実装次第ですが、僕の実装だと受信にDMAを使うことにしたので、RX用DMAを有効にする。転送先メモリはリングバッファとして使用するため、モードはCircularに設定し、メモリアドレスは自動でインクリメントされるようにIncrement Addressにチェックをつける
8. 他のタブはそのまま (僕の実装だと今回は割り込みは使いませんでした。また、送信も割り込み/DMAは面倒だったので未使用です)
プロジェクトの設定
-
設定を保存する
メニューバー -> Project -> Settings で設定する。ここで指定した保存先に、CubeMXの設定ファイル(*.iocファイル)と後でエクスポートするプロジェクトファイル一式が保存される。下記設定でひとまずOK。他のタブはデフォルトのまま。
-
メニューバー -> Project -> Generate Code でCubeMXで設定したプロジェクトファイルのテンプレートがエクスポートされる。自動的にSW4STM32(Eclipse)が起動される。後々、ハードウェア設定を変更する際には、iocファイルを開いて、設定変更後、再度Generate Codeをする。
LEDチカチカ実装 (SW4STM32)
CubeMXでコード生成したプロジェクトを使う際の注意点
自分のコードは必ず下記コメントの中に書く。でないと、再度CubeMXでコード生成したときに消されてしまう。
/* USER CODE BEGIN 1 */
My Code comes here
/* USER CODE END 1 */
LEDチカチカコード
自動生成されたmain.cの中のmain関数にLED制御用コードを追加する。CubeMXで設定したクロック設定やペリフェラル設定を行うコードがあるが、そこはひとまず無視。
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の送受信関数さえあればいいのでドライバ自体はどのように実装しても良いです。ちなみに、この実装はいろいろと手を抜いています。オーバーフローや、タイミング依存の排他処理などを考慮していません。
#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_ */
#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_ */
#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するようにする
/* 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で接続する(ドライバは他のソフトウェアをインストールした時に自動でインストールされていたと思うのですが、無かったらググってください。)。
ボーレートを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を用意してあげます。
-
sw4stm32(eclipse)のメニューバーでFile -> New -> C Project -> Ac6 STM32 MCU Project (Toolchains = Ac6 STM32 MCU GCC)
2. プロジェクト名は適当でいいです。どうせ後で消します。
-
MCU Configurationを以下のようにします。これも適当でいいとは思うのですが、一応使用しているボードに合わせます。そして、ここではFinishではなく、Nextをクリック
-
新たに作られたプロジェクトのsrcフォルダ内にsyscalls.cがあるので、それを今回のプロジェクトのSrcフォルダにコピーします。
一時的に作ったプロジェクトはもう消してしまって大丈夫です。ちなみに、CubeMXを使わないでプロジェクトを作るときはこのような手順になります。
ポーティング
syscalls.cを軽く見ると、_read
、_write
関数の中で__io_getchar
と__io_putchar
関数を呼んでいます。この2関数を実装することがポーティング作業になります。同じファイル内にベタ実装してもいいのですが、今回はたまたまprintfの出力先としてUARTを使いますが、場合によっては液晶ディスプレイや別のデバイスにすることもあると思います。なのでファイルは分けてあげて、後々簡単に切り替えられるようにした方が良いと思います。僕はよくretarget.cという形にしています。retarget.cというファイルをSrc下に新たに作ります。
#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も使えます。
/* 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);
とかすると、何も表示されません。リンカの設定を下記のように変えてあげる必要があります。こちらを参考にさせていただきました。
とはいえ、通常の組み込みソフトウェアの実装にも言えることですが、floatはあまり使わない方がいいです。このオプションを有効にするだけでバイナリサイズがだいぶ増えます。
text data bss dec hex filename
12408 112 1776 14296 37d8 BasicUart.elf
text data bss dec hex filename
21528 476 1772 23776 5ce0 BasicUart.elf