突然ですが、main関数は一体誰が呼んでいるのでしょうか。
普通に考えるとプログラムはROMの0番地から始まりますから0番地にmainがあるのでしょうか。
違います。mainは別の場所にあります。0番地ではない。
実は今まで僕らがマイコンに書き込んできたプログラムには「main関数を呼ぶ別のプログラム」がまざっていて、そいつが最初に動いています。
しかもそいつはクロックの源を決めたり速度まで変更してくれちゃってます。
正体を確かめておきましょう。短いプログラムです。
犯人は誰なのか
CMSISを覚えてるでしょうか。
僕が「CoIDEってすごいよ!」とか騒いでる時に、プロジェクトを作って最初にやったことです。ここです。
ここで外部からプログラムを取り込みました。前回まで使ってたプロジェクトのプロジェクトナビゲーションにも入っています。
これらは僕らのmain.cといっしょにマイコンに書き込まれます。
既にstm32f407xx.hについてはレジスタが沢山defineされてるんだよ〜っていう話をしました。
他にもいろいろいますが、大体.hというヘッダファイルですから定義であってプログラムではありません。
ただ、そんな中
- startup_stm32f407xx.S
- system_stm32f4xx.c
という連中がいます。上はアセンブリ言語、下はC言語のプログラムです。
実はこのアセンブリがmainを呼ぶ実体です。
startup_stm32f407xx.Sを見る
このアセンブリを開いて見てみるとどうやら上から順番に実行されるわけではありません。
「この辺は0番地にこの辺は100番地に」という番地指定がされているのでややこしくなっています。実際のプログラムは0番地から実行されるわけですから0番地から見てみましょう。
.word _eram .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word DebugMon_Handler .word 0 .word PendSV_Handler .word SysTick_Handler /* External Interrupts */ .word WWDG_IRQHandler /* Window WatchDog */ .word PVD_IRQHandler /* PVD through EXTI Line detection */ .word TAMP_STAMP_IRQHandler /* Tamper and TimeStamps through the EXTI line */ .word RTC_WKUP_IRQHandler /* RTC Wakeup through the EXTI line */
途中を省略していますがこれが0番地からのプログラムです。
実はこれプログラムではないです。
0番地は _eramで指定される32bitの数字を、1番地にはReset_handelrで指定される32bitの数字を置いておくというただの固定値です。
これは割り込みベクターテーブルと言って「割り込みが起きた時にどのアドレスに飛ぶか」を示したものです。他のマイコンを触ったことがある人なら知っているはず。
ここではそれがこんな風に書かれているわけです。
ちなみに最初の_eramというのはスタックポインタの初期値です。
リンク時にスタックポインタの最初のアドレスが決まりそれが_eramとなります。
ARM-Cortex-Mでは0番地はプログラムでなくスタックポインタのアドレスというのは決まっています。起動した直後からスタックを使えるようにするため特別になっています。
さて、ROMの上ののほうが割り込みベクタなのはわかりました。ではマイコンは起動後どう動くかというと
- 0番地のアドレスを読みスタックポインタを設定する
- 割り込みベクターテーブルのリセットハンドラへ飛ぶ
ここで割り込みベクターテーブルのリセットハンドラとはリセット割り込みがかかった時にジャンプする先のことで、ベクターテーブルの一番上です。つまりResetHandlerのことです。
これはラベルと言ってC言語では関数みたいなもんです。
これはどこなのかとこのアセンブリを調べると上の方にありました
Reset_Handler: ldr sp, =_eram /* set stack pointer */ /* Copy the data segment initializers from flash to SRAM */ movs r1, #0 b LoopCopyDataInit CopyDataInit: ldr r3, =_sidata ldr r3, [r3, r1] str r3, [r0, r1] adds r1, r1, #4 LoopCopyDataInit: ldr r0, =_sdata ldr r3, =_edata adds r2, r0, r1 cmp r2, r3 bcc CopyDataInit ldr r2, =_sbss b LoopFillZerobss /* Zero fill the bss segment. */ FillZerobss: movs r3, #0 str r3, [r2], #4 LoopFillZerobss: ldr r3, = _ebss cmp r2, r3 bcc FillZerobss /* Call the clock system intitialization function.*/ bl SystemInit /* Call static constructors */ bl __libc_init_array /* Call the application's entry point.*/ bl main bx lr
こっちはちゃんとプログラムです。
見えてきましたね。アセンブリが詳しくない人のために解説するとここでやっていることは
- 初期値のあるグローバル変数の値をセット
- 初期値のないグローバル変数に0をセット
- SystemInit を呼ぶ
- __libc_init_arrayを呼ぶ
- mainを呼ぶ
となっています。そう!下の方にmain!。いや〜、mainを読んでいるところを見つけましたね!
ただ、mainまでにもいろいろやってるみたいです。
まず最初の1と2はRAMに初期値を入れていますね。
変数はRAMなんですが、RAMというのは起動後の値は不明な値が入ってます。0ですらないし、もちろん自分の欲しい値を入れておくためにはどっかでセットしないといけないのです。
グローバル変数(C言語でどの関数からも使える変数)に初期値がある場合はmainが始まっちゃう前にセットしないといけないわけです。
なのでその値はROMにセットされていて(この辺はリンカがやってくれる)起動後のこのプログラムでRAMにセットするようになっています。
そして初期値のないグローバル変数には0を入れる処理が入っています。
SystemInit
グローバル変数の初期化が終わったらmainを呼べるのですが、ここでSystemInitってのを呼んでます。これはsystem_stm32f4xx.cの中にある関数です。
このcファイルはは基本的に「クロックを決める」機能しかありません。
実際system_stm32f4xx.hをみるとこの中にあるのは
- SystemCoreClock コアのクロック数が保存してあるグローバル変数
- SystemInit クロックの設定用関数
- SystemCoreClockUpdate 呼ぶと各種レジスタを読んでSystemCoreClock変数をupdateする
となっています。さっそくSystemInit関数を見てみると
void SystemInit(void) { /* FPU settings ------------------------------------------------------------*/ #if (__FPU_PRESENT == 1) && (__FPU_USED == 1) SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */ #endif /* Reset the RCC clock configuration to the default reset state ------------*/ /* Set HSION bit */ RCC->CR |= (uint32_t)0x00000001; /* Reset CFGR register */ RCC->CFGR = 0x00000000; /* Reset HSEON, CSSON and PLLON bits */ RCC->CR &= (uint32_t)0xFEF6FFFF; /* Reset PLLCFGR register */ RCC->PLLCFGR = 0x24003010; /* Reset HSEBYP bit */ RCC->CR &= (uint32_t)0xFFFBFFFF; /* Disable all interrupts */ RCC->CIR = 0x00000000; #if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM) SystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */ /* Configure the Vector Table location add offset address ------------------*/ #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */ #else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */ #endif }
などといろいろなレジスタをいじっていて、いろいろな設定を指定ますが基本は
「内臓クロックをメインクロックとして利用する」
という設定をしています。
Discoveryボードには外付けの8Mhzクリスタルがあるのに、マイコンに内蔵されているクロックを使うようになってるわけです。そして、この設定が終わったらアセンブリに一度戻ってmainに戻っています。
結局起動するまでにしていること
mainまでの流れを見てみると
- (startup_stm32f407xx.S)グローバル変数の値をセット(初期値のあるものと0のもの)
- (system_stm32f4xx.c)SystemInitでクロックなどを設定
- (startup_stm32f407xx.S)main関数を呼ぶ
となっていたわけですね。
これでCMSISで持ってきたこのプログラムの意味と最初のクロックの設定がわかりました。
そしてどうやらクロックは内臓のものをつかうようになっているようですし、168Mhzでもうごいてなさそうです。
次回はせっかくある外部クロックへの切り替えと168Mhzで動かすための設定をしてみましょう。
まとめ
- startupスクリプトとしてアセンブリとcのファイルがある
- アセンブリは割り込みベクターテーブルとグローバル変数の初期化がある
- cのsysteminit関数がmainの前に呼ばれている