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をたたくとUSART Driver経由してシリアルに「Hello World」が出力されるイメージ。
USART Driverを作る
今回は学習のために自作することにした。
USARTを使用するには
BoardにはST-Linkが搭載されており、USB端子でUSART通信ができるためそれを使用することにした。
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は次のようになっている。
これにより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は次にようになっている。
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は次のようになっている。
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をよく読んでみたら、制御手順が載っていた。
送受信制御は以下の流れになる。
送信制御
- USART_CR1レジスタのM bitからワード長を設定する。
- USART_BRRレジスタを使用して、ボーレートを選択する。
- USART_CR2レジスタでstop bitを設定する。
- USART_CR1レジスタのUEビットに1を書き込んで、USARTを有効にする。
- USART_CR1のTEビットをセットして、最初の送信とアイドルフレームを送信する。
- 送信するデータをUSART_TDRレジスタに書き込む(これによりTXEビットがクリアされる)。
- USART_TDRレジスタにデータを書き込んだ後、TXE=1になるまで待つ。これは、TXがEmptyであること(送信完了)を示す。
受信制御
- USART_CR1レジスタのM bitからワード長を設定する。
- USART_BRRレジスタを使用して、ボーレートを選択する。
- USART_CR2レジスタでstop bitを設定する。
- USART_CR1 レジスタの UE ビットに 1 を書き込んで、USART を有効にする。
- 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レジスタにUSARTDIVを設定する。USARTDIVは次の式で求められる。
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;
}
printfでのhello world出力に成功した!!
コード
ここまでのコードはGithubにおいた。
stm32/v02
最後に
hello worldまでは長く険しい道のりだった。
しかし、今やprintfができる環境になった。
これで開発がしやすくなる。
作っている間に、クロックの設定やら割り込みやらを使用していないため、
性能を全然活かせていないことに気付いた。