前回割り込みを使わずにタイマーをつかいました。今回は割り込みを使ってみましょう。
割り込みって何
マイコンが他の何かに繋がっている時、例えばロボットを作っているとしてそれを制御するマイコンにロボットの触覚センサーが繋がっている時、触覚に何かがぶつかってからすぐに反応しないと困る時があります。ぶつかっちゃうとか落ちちゃうとか。
そうならないようにするには頻繁に触覚センサーをチェックしないといけません。
たとえばwhile(1){}の無限ループの中で
while(1){
if(触覚になにかあたった){
避ける
}
}
と言ったプログラムを入れてとにかくチェックすることになりますが、当然ロボットは他の処理もするので
while(1){
if(触覚になにかあたった){
避ける
}
他の処理
}
ということになります。
そうするとこの「他の処理中」、触覚になにかあたっても気づくことが出来ません。その間はif文が実行されないからです。超鈍感。
それを解決するのが割り込みという仕組みです。
割り込みというのは何かをきっかけに起きる緊急事態で、今やっているプログラムをいったん止めておいて、その時だけ別のプログラムを実行することが出来ます。そしてそれが終わったら元のプログラムに戻ります。書き方としてはこんな感じ。
while(1){
他の処理
}
void 触覚になにかあたったら(void)
{
避ける
}
必要なときだけ関数が呼ばれるので、これなら普通の時は無駄に触覚のチェックを毎回しなくていいし、しかも触覚に触った瞬間にするべき行動をすぐに取ることが出来ます。
whileの中のプログラムは割り込みのことは全く気にしなくても大丈夫です。途中で中断されても計算がおかしくなることはありません。(ただ、中断されるのでタイミングがズレることはあります)
これをマイコンがどうやってやっているかは今回は置いておいて、前回やったタイマーでオーバーフローがあるごとに割り込みでLEDをON・OFFさせるってのをやってみましょう。
NVICとは
STMマイコンの割り込みはNVICという仕組みが管理しています。
実はこれARM(あーむ)といわれるマイコン全てで共通の仕組みなのでSTMだけじゃなく他のARMマイコンでも同じです。
割り込みは同時に2つ発生したり(タイマ6とタイマ2が同じタイミングでオーバーフローしたり)割り込みの作業中に別の割り込みが発生することがあります。そういう時どうするかを管理するのがNVICです。
難しいことは今回は置いておきますが、NVICが全ての割り込みを管理していて、タイマ6の割り込みを許可するかどうかもNVICが管理しています。それだけ覚えれば大丈夫です。
割り込みのきっかけはGPIOでボタンが押されたら、とかA/Dの結果がある値を超えたらとか、いろいろきっかけがありますが、NVICは立ち上がり直後は全て割り込み禁止になっています。使いたいのがあれば個別にONにする必要があるのです。
NVICのISERレジスタの対応するビットを1にすれば割り込みが許可されます。
呼ばれる関数
では、許可できたとして、割りこまれた時にどの関数を呼ぶかというのはどうやって設定するのでしょうか。
実はそれスタートアップスクリプトに書いてありました。
startup_stm32f407xx.Sをもう一度見てみましょう。
g_pfnVectors:
.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 */
.word FLASH_IRQHandler /* FLASH */
.word RCC_IRQHandler /* RCC */
.word EXTI0_IRQHandler /* EXTI Line0 */
.word EXTI1_IRQHandler /* EXTI Line1 */
.word EXTI2_IRQHandler /* EXTI Line2 */
.word EXTI3_IRQHandler /* EXTI Line3 */
こんなのがあったと思います。
これが割り込みベクタ(の一部)です。
NVICは割り込まれた時にその割り込みに応じて決められたアドレス時に自動的にジャンプします。
このアセンブリでは割り込みごとにどのアドレスに飛ぶかが書いてあります。
タイマ6がどれかというとこれです。
.word TIM6_DAC_IRQHandler /* TIM6 and DAC1&2 underrun errors */
これはタイマ6のオーバーフローが起きた時に飛ぶ先としてTIM6_DAC_IRQHandlerへ飛べという設定です。
(このプログラムは”この場所に”書く必要があります。「タイマ6の割り込みジャンプ先は0x64番地に書いてある」というふうに設定してあるからです。だから割り込みベクタ”テーブル”という名前がついています)
じゃあそのとび先であるTIM6_DAC_IRQHandlerはどこなんだと思ってこのプログラムの下の方を見ると
.weak TIM6_DAC_IRQHandler
.thumb_set TIM6_DAC_IRQHandler,Default_Handler
こういうのがあります。
このweakというのは
「実際にTIM6_DAC_IRQHandlerというのがあれば TIM6_DAC_IRQHandlerはそれにする。だけどもしなければDefault_Handlerにする」という意味です。
これはつまり void TIM6_DAC_IRQHandler(void)っていう関数がどこかにあればそれを設定するけど、もし無いならDefault_Handlerにするってことです。
ちなみにDefault_Handlerはこのアセンブリの上の方にあります。見てみて下さい。ただの無限ループです。
これで設定の仕方がわかりました。
void TIM6_DAC_IRQHandler(void)
って言う名前の関数を用意すればそれでいいのです。
やってみる
それではやってみましょう。
#include "stm32f4xx.h"
static void SetSysClock(void);
int main(void)
{
SetSysClock();
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
GPIOA->MODER = 0;
GPIOD->MODER = 0x40000000;
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
TIM6->PSC = 9999;
TIM6->ARR = 4800 * 2;
TIM6->CNT = 0;
TIM6->CR1 |= TIM_CR1_CEN;
TIM6->DIER = TIM_DIER_UIE;
NVIC_EnableIRQ(TIM6_DAC_IRQn);
while(1);
}
void TIM6_DAC_IRQHandler(void)
{
TIM6->SR = 0;
GPIOD->ODR = ~(GPIOD->ODR);
}
#define PLL_M 8
#define PLL_N 336
#define PLL_P 2
#define PLL_Q 7
#define PLLI2S_N 258
#define PLLI2S_R 3
static void SetSysClock(void)
{
__IO uint32_t StartUpCounter = 0, HSEStatus = 0;
RCC->CR |= ((uint32_t)RCC_CR_HSEON);
do {
HSEStatus = RCC->CR & RCC_CR_HSERDY;
StartUpCounter++;
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
if ((RCC->CR & RCC_CR_HSERDY) != RESET) {
HSEStatus = (uint32_t)0x01;
} else {
HSEStatus = (uint32_t)0x00;
}
if (HSEStatus == (uint32_t)0x01) {
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
PWR->CR |= PWR_CR_VOS;
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // APB2は84Mhz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV4; // APB1は 42Mhz
RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) | (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24); RCC->CR |= RCC_CR_PLLON;
while((RCC->CR & RCC_CR_PLLRDY) == 0);
FLASH->ACR = FLASH_ACR_ICEN |FLASH_ACR_DCEN |FLASH_ACR_LATENCY_5WS;
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL);
} else {
GPIOD->ODR |= 1<<14; } RCC->CFGR &= ~RCC_CFGR_I2SSRC;
RCC->PLLI2SCFGR = (PLLI2S_N << 6) | (PLLI2S_R << 28); RCC->CR |= ((uint32_t)RCC_CR_PLLI2SON);
while((RCC->CR & RCC_CR_PLLI2SRDY) == 0);
}
前回と違うのはここですね
TIM6->DIER = TIM_DIER_UIE;
NVIC_EnableIRQ(TIM6_DAC_IRQn);
while(1);
まずタイマ6のDIERレジスタのUIEビットを1にしています。
これは”タイマー6ペリフェラルにおける割り込みをするかどうか”の設定になります。
これが1になっているとオーバーフローが起きた時に割り込み要求がNVICの方へ行きます。(もっと正確に言うとSRレジスタのUIFビットがNVICに割り込みを要求するようになります)
ただ、それだけだとNVICは受け付けてくれません。こんどは”NVICにおけるタイマー6の割り込みを許可するかどうか”の設定が必要になります。
NVICのISERレジスタをいじればよいのですが、便利な関数が用意されているのでそれを利用します。
この2つによって割り込みが使えるようになります。
割り込み時の流れを確認すると
- タイマ6カウンタがARRと同じ値になりその後オーバーフローして0になる
- SRレジスタのUIFが1になる
- DIERレジスタのUIEが1なのでNVICに対して割り込み要求が行く
- NVICではタイマ6の割り込みが許可されているので、今のプログラムの実行をいったん停止してvoid TIM6_DAC_IRQHandler(void)関数を呼び出し実行する
- void TIM6_DAC_IRQHandler(void)関数が終わったら何事もなかったかのようにさっき停止した場所に戻リ続ける
となります。
その割り込み関数の中では
void TIM6_DAC_IRQHandler(void)
{
TIM6->SR = 0;
GPIOD->ODR = ~(GPIOD->ODR);
}
といった感じでSRレジスタの0セットとLEDの反転をしています。
SRレジスタのUIFを0にするのは重要です。
割り込み要求はSRレジスタが1だからこそ起きるので、これを0にしないとこの関数が終わった時に「お、SRレジスタが1だ。じゃあ割り込もう」ということでもう一度この関数に戻ってしまいます。なので、すぐさま0に戻します。
どうでしょうか。意外とあっさり割り込みに出来たと思います。
無限ループを見てみて下さい。while(1);となっていて、何もしていません。
必要なときに(タイマ割り込みで)LEDのON・OFFが切り替わる関数が呼ばれるので非常に便利ですね。
まとめ
- 割り込みというプログラムを停止して特別に今だけ関数を呼び出す仕組みがある
- 割り込みはNVICで管理されている
- 割り込みするにはペリフェラルの割り込み許可とNVICの割り込み許可が必要
- 割り込みで呼ばれる関数の名前は決まっている(スタートアップスクリプトによって)
- 割りこまれたら割り込みの元になるフラグは0にすぐ戻す