#はじめに
STM32 Nucleo Boardでブートローダを作るでブートローダとメインプログラムを分離しました。
しかし、そのままではメインプログラム側で割り込み制御ができないようです。
解決しておきたい課題が増えました。
#開発ターゲット
STM32F303K8
#割り込みベクタテーブルとは
マイコンには多数の割り込み要因があり、それぞれに対して割り込みハンドラを用意します。
割り込み要因を特定するために、マイコンでは各要因に固有の番号(割り込みベクタ)を割り当ています。
割り込みベクタと、対応する割り込みハンドラの先頭アドレスを一覧表にまとめたものを、割り込みベクタテーブルと呼びます。
システム情報として、ROM領域にテーブルを作成することが多いです。
#やること
STM32 Nucleo Boardでブートローダを作るにてブートローダからRAM上のメインプログラムにジャンプする構造にしました。
しかし、ROM領域に割り込みベクタテーブルと割り込みハンドラを配置しているため、そのままではメインプログラムで割り込みを処理することができません。
ここでは、メインプログラムにある割り込みハンドラを使用できるように工夫した実装を行います。
#方針
組み込みOS自作入門にて次のような実装が紹介されていました。
①割り込みハンドラはブートローダ側で用意して、割り込みベクタにはそのハンドラのアドレスを設定する。
②RAM上に用意した専用の領域にメインプログラム側の割り込みハンドラのアドレスを書いておく。(割り込みベクタテーブル(RAM))
③ブートローダの割り込みハンドラでは、RAM上に用意した割り込みベクタテーブルに書かれているアドレスを見て、そこにジャンプする。
ここでも、この方針で実装を行いたいと思います。イメージは以下のようになります。
RAM上の専用領域に設定するアドレスをメインプログラムで自由に設定することで、割り込みハンドラを自由に登録できるようになります。
#作る
方針を元にプログラムを作成していきます。ソースコード全体はこちら。
##メモリ構成を決める
とりあえず、割り込みベクタテーブル(RAM)用に1KB確保しておきます。
最大256個のアドレスが登録できる計算です。
##リンカスクリプト変更
上記のメモリ構成に変更します。ブートローダ、メインプログラムの両方で使用するため領域名はRAM_COMMONとしています。
/* Specify the memory areas */
MEMORY
{
RAM_BOOT (xrw) : ORIGIN = 0x20000000, LENGTH = 1K
RAM_COMMON (rw) : ORIGIN = 0x20000400, LENGTH = 1K /* この領域を追加 */
RAM_CODE (xrw) : ORIGIN = 0x20000800, LENGTH = 6K
RAM_WORK (rw) : ORIGIN = 0x20002000, LENGTH = 4K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 4K
FLASH_BOOT (rx) : ORIGIN = 0x08000000, LENGTH = 8K
FLASH_MAIN (rx) : ORIGIN = 0x08002000, LENGTH = 56K
}
RAM_COMMONのアドレスをプログラムで使用するため、セクションを定義し、シンボルを作成しておきます。
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH_BOOT
.common :
{
_common = .; /* シンボルを作成 */
} >RAM_COMMON
:
省略
:
##割り込み制御モジュール追加
割り込み制御モジュールを追加します。このモジュールはブートローダとメインプログラムの両方に追加します。
たくさんの割り込み要因がありますが、メインプログラムで使用したい割り込みについてテーブルを作成すれば良いです。
ここではUSARTの割り込みとTimerの割り込みに対応します。
#ifndef _INTR_DRIVER_H_
#define _INTR_DRIVER_H_
/* 割り込み種別の定義 */
typedef enum{
INTR_TYPE_USART,
INTR_TYPE_TIMER,
INTR_TYPE_NUM,
}intr_type_t;
extern int _common; /* リンカスクリプトで定義したRAM_COMMON領域の先頭アドレス */
#define SOFTVEC_ADDR (&_common)
typedef void (*intr_handler_t)(void); /* 割り込みハンドラの関数ポインタ */
#define SOFTVECS ((intr_handler_t*)SOFTVEC_ADDR) /* 割り込みハンドラの先頭アドレス */
/* 割り込みベクタテーブルの初期化 */
int IntrInit(void);
/* 割り込みハンドラの登録 */
void IntrHandlerSet(intr_type_t type, intr_handler_t handler);
/* 登録された割り込みハンドラを実行 */
void IntrHandlerExec(intr_type_t type);
/* ブートローダで呼ばれる割り込みハンドラ */
void USART2_IRQHandler(void);
void TIM6_DAC1_IRQHandler(void);
#endif
#include "intr_driver.h"
#include <stdlib.h>
int IntrInit(void)
{
int type;
for(type = 0; type < INTR_TYPE_NUM; type++){
IntrHandlerSet(type, NULL);
}
return 0;
}
void IntrHandlerSet(intr_type_t type, intr_handler_t handler)
{
SOFTVECS[type] = handler;
}
void IntrHandlerExec(intr_type_t type)
{
intr_handler_t handler = SOFTVECS[type];
if(handler){
handler();
}
}
void USART2_IRQHandler(void)
{
IntrHandlerExec(INTR_TYPE_USART);
}
void TIM6_DAC1_IRQHandler(void)
{
IntrHandlerExec(INTR_TYPE_TIMER);
}
IntrInit()
は割り込みベクタテーブル(RAM)の初期化です。_commonからハンドラ数の分をNULLにします。
IntrHandlerSet()
は割り込みハンドラの登録を行います。_commonをベースにtypeで指定した位置にハンドラが登録されます。
IntrHandlerExec()
は割り込みハンドラの実行を行います。_commonをベースにtypeで指定した位置のハンドラが実行されます。
USART2_IRQHandler()
、TIM6_DAC1_IRQHandler()
は割り込みハンドラです。
これらのハンドラはブートローダで定義されているためROMに配置されます。しかし、実態は間接参照による関数実行となります。
参照先にメインプログラムの関数を登録しておけば、割り込み発生時にその関数が実行されます。
一般に割り込み処理は、割り込まれる前の状態を保持するため、アセンブラでレジスタ退避処理を書く必要があります。
しかし、Coretex-Mではハードウェアでその処理を行っているため、C関数のみ書けばOKです。
##メインプログラム作成
テストとして、タイマ割り込みが使用できているか確認するためのプログラムを作成します。
###タイマ制御モジュール
タイマ制御の詳しい説明はSTM32 Nucleo BoardでTimerを使うを参照してください。
ここでは初期化処理で割り込みハンドラの登録処理を追加しています。
#include "timer_driver.h"
#include "intr_driver.h"
#include "printf.h"
void (*callback)(void);
void TimerIntrHandler(void)
{
if(callback != NULL){
callback();
}
TIM6->SR = 0;
}
void TimerInit(void)
{
RCC->APB1ENR &= ~RCC_APB1ENR_TIM6EN;
TIM6->PSC = 0;
TIM6->ARR = 0;
TIM6->CNT = 0;
TIM6->CR1 &= ~TIM_CR1_CEN;
TIM6->DIER &= ~TIM_DIER_UIE;
NVIC_DisableIRQ(TIM6_DAC_IRQn);
callback = NULL;
IntrHandlerSet(INTR_TYPE_TIMER, TimerIntrHandler);
}
static void TimerStart_sec(int timeout_sec)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
TIM6->PSC = 9999;
TIM6->ARR = 800 * timeout_sec;
TIM6->CNT = 0;
TIM6->CR1 |= TIM_CR1_CEN;
TIM6->DIER = TIM_DIER_UIE;
NVIC_EnableIRQ(TIM6_DAC_IRQn);
}
// add timer counting by seconds
int TimerAdd_sec(int timeout_sec, void (*function)(void))
{
if(timeout_sec > 80){
printf("Error : Over 80 seconds");
return -1;
}
callback = function;
TimerStart_sec(timeout_sec);
return 0;
}
TimerInit()
でIntrHandlerSet(INTR_TYPE_TIMER, TimerIntrHandler);
を呼び出し、
メインプログラムの関数であるTimerIntrHandler()
を登録しています。
###テスト用メインプログラム
#include "printf.h"
#include "timer_driver.h"
int main(void)
__attribute__ ((section (".entry_point")));
void timeup_function(void)
{
printf("timeup!!\n");
}
// Main -----------------------------------------------------------------------
int main(void)
{
__disable_irq(); // disable interrupt
TimerInit();
__enable_irq(); // enable interrupt
printf("Main Program booted!!\n");
TimerAdd_sec(1, timeup_function);
while(1);
return 0;
}
割り込み設定中に割り込みがあると困るので設定中は割り込み禁止にしています。
__disable_irq()
、__enable_irq
はARMコンパイラのみの関数です。
1秒ごとにタイムアップしたら割り込みが入り、timeup_function()
が呼ばれるようにしています。
#テスト
ブートローダからメインプログラムを起動します。
「timeup!!」が1秒ごとに表示!!
#おわりに
よく調べてみるとARMにはVTORレジスタなるものがあるようです。
これを使えばもっと楽に割り込みベクタテーブルをRAMに配置することができそうな気がする。
今後の課題とします。
#参考書籍
12ステップで作る組み込みOS自作入門