C
ARM
HelloWorld
組み込み
STM32

STM32 Nucleo Boardでベアメタルなhello world

STM32 Nucleo Boardでベアメタルなhello world

これまで開発環境を構築し、実行ファイル作成までできるようになった。
しかし、私はなぜかCUI開発環境なので、GDBデバッグだけでは効率的でないと思っていた。
ここはひとつ「printf」を作るしかない。
今回はSTM32 Nucleo Boardでベアメタルな「hello world」を実現するしかない!

開発ターゲット

STM32 Nucleo Board STM32F303K8

STM32 Nucleo Boardでhello worldするには

STM32 Nucleo Boardには外部ディスプレイがない。しかし、USARTが使用可能。
ということで今回はシリアル出力で「hello world」という文字列を表示させる。

完成イメージ

printf.png
printfをたたくとUSART Driver経由してシリアルに「Hello World」が出力されるイメージ。

USART Driverを作る

今回は学習のために自作することにした。

USARTを使用するには

BoardにはST-Linkが搭載されており、USB端子でUSART通信ができるためそれを使用することにした。
vusart.png
SB2とSB3は購入時でONだったので PA2 → USART TX(送信部) PA15 → USART RX(受信部)となる。
USB端子なので、はんだ付けしなくていいなとは思ったけど、そこから先はよく分からず。
yuki-sato.comを参考にさせてもらった。

実はUSART専用のピンはなくて、GPIOとの共有ピンになっていてGPIOの設定をすることでUSARTピンとして切り替えて使います。
切り替えると当然ながらGPIOとしては入力も出力も使えなくなるのですが、USARTのピンとして使えるようになります。
ただ、「じゃあPortAの0をTx(送信)にする!」みたいにどのピンでもUSARTのピンに出来るわけではありません。
まず、ピン1つに16種類までの機能が割り振られていて、その16種類のどれかに切り替えるという感じになります。

なるほど。さっきのPA2ピンとPA15ピンをUSARTに切り替える必要があるのか。
データシートをみるとGPIO PortAは次のようになっている。
gpio_af.png
これによりGPIOAの2番ピンをAlternateFunction7、GPIOAの15番ピンをAlternateFunction7に設定すると、
それぞれUSART2_TX、USART2_RXとなることが分かる。

GPIOピンは次の2つの設定を行うことでUSARTピンとして使用できるようになる。
(1)GPIOモードの設定
(2)GPIO AlternateFunctionの設定

GPIOモードの設定

GPIO port mode registerからGPIOモードを設定する。
ReferenceManualではGPIO port mode registerは次にようになっている。
gpio_mode.png
PA2/PA15をAlternativeFunctionモードに切り替えるにはMODER2に10/MODER15に10を設定する。

GPIO AlternateFunctionの設定

GPIO alternate function registerからAlternateFunctionを設定する。このレジスタは1ピンを4bitで設定する。
そのため32bitでは足りず、low registerとhigh registerに分かれている。
ReferenceManualではGPIO alternate function low registerは次のようになっている。
gpio_afrl.png
PA2/PA15をAlternativeFunction7に切り替えるにはGPIOA_AFRLのAFR2に0111/GPIOA_AFRHのAFR15に0111を設定する。

USARTピンを切り替えるコード

ここまでをコードにしてみた。
レジスタの定義などはstm32f303x8.hに詳細に定義されているため、
それを使用している。

void EnablePinUSART(void)
{
    RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

    GPIOA->MODER = 0;
    GPIOA->AFR[0] = 0;
    GPIOA->AFR[1] = 0;

    // PA2 and PA15 as AF
    GPIOA->MODER |= GPIO_MODER_MODER2_1;
    GPIOA->MODER |= GPIO_MODER_MODER15_1;

    // Select AF7 (USART2) for PA2 and PA15
    GPIOA->AFR[0] = 0x00000700;
    GPIOA->AFR[1] = 0x70000000;
}

最初の RCC->AHBENR |= RCC_AHBENR_GPIOAEN; はGPIOAのクロックを有効にしている。
これでピンをUSARTとして使えるようになった。

USARTでのデータ送受信制御を実装する

ここから先はいよいよUSARTの制御になる。しかし何をどうしたらよいか分からない。
そこでReference Manualをよく読んでみたら、制御手順が載っていた。
送受信制御は以下の流れになる。

送信制御

  1. USART_CR1レジスタのM bitからワード長を設定する。
  2. USART_BRRレジスタを使用して、ボーレートを選択する。
  3. USART_CR2レジスタでstop bitを設定する。
  4. USART_CR1レジスタのUEビットに1を書き込んで、USARTを有効にする。
  5. USART_CR1のTEビットをセットして、最初の送信とアイドルフレームを送信する。
  6. 送信するデータをUSART_TDRレジスタに書き込む(これによりTXEビットがクリアされる)。
  7. USART_TDRレジスタにデータを書き込んだ後、TXE=1になるまで待つ。これは、TXがEmptyであること(送信完了)を示す。

受信制御

  1. USART_CR1レジスタのM bitからワード長を設定する。
  2. USART_BRRレジスタを使用して、ボーレートを選択する。
  3. USART_CR2レジスタでstop bitを設定する。
  4. USART_CR1 レジスタの UE ビットに 1 を書き込んで、USART を有効にする。
  5. USART_CR1 レジスタの RE ビットをセットする。これによりレシーバが有効になり、Start bitの検索を開始する。

データが受信されると、
・RXNE ビットは、RDR レジスタにデータが転送されるとセットされる。読み出し可能ということ。
・RXNE ビットのクリアは、ソフトウェアによる USART_RDR レジスタの読み出しによって行われる。

実装方針

上記の送受信で1.~5.はほぼ共通のため、初期設定としてまとめることができそう。
TXEビットとRXNEビットはUSART_ISRレジスタにあり、監視を行いながら制御する。
割り込みを使った方法もあるみたいだが、まずは簡単な方法で実装する。
各設定値は、次のようにする。
ワード長:8bit (デフォルト)
stop bit:1bit (デフォルト)
ボーレート:115200bps (なんかよく使われてる気がするから)
ボーレートの選択方法はReference Manualに書いてあったので、次項で説明する。

ボーレートの設定

送信/受信制御の「2.」であるように、USART_BRRでボーレートを設定する。
今回は非同期通信で、クロックによるデータの同期はない。
そのため、受信側と送信側で通信速度(ボーレート)を合わせる必要がある。
usart_brr.png

USART_BRRレジスタにUSARTDIVを設定する。USARTDIVは次の式で求められる。

calc_baudrate.png

Tx/Rx baudは通信速度で、fckはクロック。
スタートアップルーチンでシステムクロックにHSIを選択しており、USART2はのクロックは8MHzとなる。
115200にするには
USARTDIV=8000000/115200
として設定する。

USARTのデータ送受信を制御するコード

上記の設定処理と送信/受信制御をコード化してみた。

static void ConfigureUSART(void)
{
    // Distribute clock to USART2
    RCC-> APB1ENR |= RCC_APB1ENR_USART2EN;

    // Configure baudrate
    USART2->BRR  = 8000000L/115200L;

    // Eable USART, TX and RX
    USART2->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

char ReadUSART(void)
{
    // Wait for a char on the USART
    while (!(USART2->ISR & USART_ISR_RXNE));
    return USART2->RDR;
}

void WriteUSART(char c)
{
    // Wait for a char on the USART
    while (!(USART2->ISR & USART_ISR_TXE));
    USART2->TDR = c;
}

RCC->APB1ENR |= RCC_APB1ENR_USART2EN; はUSART2のクロックを有効にしている。
また、ワード長とstop bitはデフォルトのため設定していない。
これでUSARTでのデータ送受信が可能になった。
ここまでの処理をUSART Driverとする。

printfを作る

ごめんなさい。printfはA tiny printfを使用する。

USART Driverと接続

printfの初期化関数内でUSART Driver経由で文字が出力されるように関数を登録する。
次のinit_printfに出力関数が登録できる。

void init_printf(void (*putf)( char))
{
    stdout_putf = putf;
}

ここに作成したWriteUSARTをセットすればprintfでの出力がUSARTになる。
がしかし、minicomでは改行がうまくいかない問題が発生したので、USART Driverに以下の処理を追加。
このPutcUSARTをprintfにセットすることにした。

void PutcUSART(char c)
{
    if(c == '\n'){
        WriteUSART('\r');
    }
    WriteUSART(c);
}

テスト

printfでhello worldを出力するプログラムを書く。

#include "stm32f303x8.h"
#include "usart_driver.h"
#include "printf.h"

// Main -----------------------------------------------------------------------
int main(void)
{
    InitUSART();

    init_printf(PutcUSART);

    printf("Hello World\n");
    printf("Hello World\n");
    printf("Hello World\n");

    return 0;
}

プログラムを実行する。
hello_world.png

printfでのhello world出力に成功した!!

コード

ここまでのコードはGithubにおいた。
stm32/v2

最後に

hello worldまでは長く険しい道のりだった。
しかし、今やprintfができる環境になった。
これで開発がしやすくなる。
作っている間に、クロックの設定やら割り込みやらを使用していないため、
性能を全然活かせていないことに気付いた。

参考サイト

ST公式
yuki-sato.com