AVR32のMPUドライバの例題を使ってみる(その2)

AVR32のMPUドライバの例題を使ってみる(その1)からの続き。





まずはMPUドライバのIntroductionを読んでみる。形式的な内容なので「Example operating mode:」以降から。

  • この例題は、MPUARIエントリと、MPUARDエントリの数を表示することにより開始する。
  • メモリ領域は以下のやり方でセットアップする。


ユーザメモリ領域のアクセス許可

Memory address Region sub-region Access Permissions
Ram 0x00000000 - 0x00003C00 1 0 to 14 Read / Write
Ram 0x00003C00 - 0x00004000 1 15 Read
Stack &stack - &stack + stack size 2 all Read / Write
Flash 0x80000000 - 0x8000C000 0 0 to 11 Read / eXecute
Flash 0x8000C000 - 0x80010000 0 12 to 15 Read
PBA memory map 0xFFFF0000 - 0xFFFFFFFF 3 all Read / Write

  • アプリケーションがリージョン1/サブ・リージョン15へ書き込みアクセスするとき。それがDTLB書き込み保護例外をトリガする→LED3が点灯する。
  • アプリケーションがリージョン0/サブ・リージョン12の関数を呼び出すとき。それがITLB保護例外をトリガする→LED2が点灯する。
  • 例題の最後:LED0とLED1が永遠に点滅する。

更に飛ばして「Information」の箇条書き。

  • EVK1100またはEVK1101評価キット
  • CPUクロック12MHz
  • USART1をRS232C DB9ケーブル経由でPCのシリアル・ポートに接続する。
  • PCターミナル設定は、57600[bps], 8bit data, no parity bit, 1 stop bit, no flow control。



mpu_example.cを読んでみる。
先にmain()から。

507:	int main(void)
508:	{
509:	  Enable_global_exception();
510:	  Enable_global_interrupt();
511:	

関数main開始直後、例外と割込を有効にしている。509行と510行はマクロ展開されてレジスタSRのbit21(Exception Mask)とbit16(Global Interrupt Mask)をビットクリアしている(これらはcompiler.hで定義)。

514:	  gpio_set_gpio_pin(LED0_GPIO); gpio_set_gpio_pin(LED1_GPIO);
515:	  gpio_set_gpio_pin(LED2_GPIO); gpio_set_gpio_pin(LED3_GPIO);

LED消灯。

523:	  pm_switch_to_osc0(&AVR32_PM, FOSC0, OSC0_STARTUP); 

PM(Power Manager)に対してクロックをOSC0(12MHz)へ変更指示。FOSC0は12000000。

526:	  init_dbg_rs232(FOSC0);
527:	  print_dbg("\n\n## This is the AT32UC3 MPU example ##");

RS232Cを初期化。クロック源はOSC0を使用。メッセージを出力する。
以降メッセージ出力関係は省略。

551:	  test_memory_area();

メモリ領域をテストし、MPUを有効化する(後述)。

557:	  *( (volatile int*) (0x00003C00))  = 0x55AA55AA;

意図的にDTLB保護例外を発生させ、結果としてLED3が点灯する。

561:	  cpu_delay_ms(2000, FOSC0);

2秒間待って次へ進む。関数cpu_delay_msはインラインでcycle_counter.hに定義されていて、サイクル・カウンタが0になるのをポーリングしている。この例題では問題ないが、実プログラムで使用すると問題になる可能性が高い。

568:	  ForbiddenFunc();

保護されていない領域のコードを実行したら、ITLB保護例外が発生し、LED2が点灯する。

571:	  DisplayLedChaserInfinitly();

LED0とLED1を点滅させる。

MPUの使用法は結局初期化(設定)とハンドラが中心となるので、関数mainはあまり重要ではない。



次はMPUを初期化している初期化関数test_memory_areaを読んでみる。

321:	void test_memory_area(void)
322:	{

337:	  *(U32 *)&config1 = (U32)Get_system_register(AVR32_CONFIG1);
338:	  u8NbMpuAriEntries = config1.immusz;
339:	  u8NbMpuArdEntries = config1.dmmusz;

システム・レジスタCONFIG1を読み出してきて、その中のIMMUエントリ数とDMMUエントリ数を、それぞれu8NbMpuAriEntriesとu8NbMpuArdEntriesに格納する。

355:	  dmpu_entries[REGION_0_FLASH_INDEX].addr = AVR32_FLASH_ADDRESS;
358:	  dmpu_entries[REGION_0_FLASH_INDEX].size = MPU_REGION_SIZE_64KB;
360:	  dmpu_entries[REGION_0_FLASH_INDEX].valid = 1;
361:	  if( set_mpu_entry(&dmpu_entries[REGION_0_FLASH_INDEX], REGION_0_FLASH_INDEX) )
362:	    ToggleLedsInfinitly();  // Error

表「ユーザメモリ領域のアクセス許可」の「Memory」における"Flash"のリージョン設定である。REGION_0_FLASH_INDEXは0(ローカル・マクロ定義)、AVR32_FLASH_ADDRESSは製品ヘッダファイル(uc3b0256.h)で定義されたFlushROMも開始アドレス、MPU_REGION_SIZE_64KBはMPUドライバ・ヘッダファイル(mpu.h)で定義されたリージョン・サイズである。
mpuに設定する情報を格納するテーブルdmpu_entriesに

  • リージョン開始アドレス、
  • リージョン・サイズ
  • 有効フラグ

を設定して、この情報をset_mpu_entry()に渡して1つのリージョンを設定する。戻り値は失敗フラグで、失敗したときにはLEDを点滅しているようだ。

366:	  set_access_permissions(REGION_0_FLASH_INDEX, MPU_APRA_ID, MPU_PRIVRX_UNPRIVRX);
369:	  set_access_permissions(REGION_0_FLASH_INDEX, MPU_APRB_ID, MPU_PRIVR_UNPRIVNONE);

ユーザメモリ領域のアクセス許可にある通り、FlushROMに対して2種類のアクセス許可を設定している。レジスタA群/B群があるから可能なのだが、逆を言えば同一リージョンに3種類のアクセス許可は出来ないかもしれない。複数リージョンを同一メモリ領域に設定すれば可能か?
第3引数でアクセス許可の種別を渡している。MPU_PRIV???_UNPRIV???という形式なのは、アーキテクチャ・マニュアルの「Table 5-3. Access permissions implied by the APn bits」を見れば一目瞭然だ。特権(Privileged)モード/非特権(Unprivileged)モード毎にアクセス許可のペアがあるからだ。

386:	  select_subregion(REGION_0_FLASH_INDEX, 0xF000); 

リージョンとそのアクセス許可を設定したので、次はそのサブ・リージョンを設定する。第2引数がレジスタMPUPSRn(nはリージョン番号)にそのまま設定される。これはビットフラグになっていて、0のときレジスタA群、1のときレジスタB群のアクセス許可が該当サブ・リージョンに適応される。再度ユーザメモリ領域のアクセス許可と見比べて欲しい。
余談だが同一ドライバ/モジュールの接頭子などは統一して欲しいものだ。
以下、リージョン領域の設定、アクセス許可設定、サブ・リージョンのA/B選択を繰り返す。これらは省略するが、スタックだけは、サイズの求め方が面倒になっている。

448:	  u32kBSizeValue = (U32)*1
454:	    ToggleLedsInfinitly();  // Error
455:	  dmpu_entries[REGION_2_STACK_INDEX].size = RegionSize;

つまりドライバ関数mpu_convert_kbsize_to_eregionsizeを使って単位変換している。しかし基本的にスタック・サイズはサブ・リージョン・サイズの1〜16倍なので、そのことを考慮してスタックを設定していれば、この関数を呼ばなくても簡単に計算できるはずだ。

499:	  enable_mpu(); // Enable the MPU address checking.

最後にMPUの機能を有効化している。
テーブルdmpu_entriesにMPUのリージョン設定情報が残っているが、実はどこからも参照されていなかったりする(怒)。



残った関数handle_exceptionである。この関数は例外/割込すべてで呼び出される。

196:	  switch(exception_cause)

203:	      disable_mpu();  // Disable the MPU to avoid to trigger the exception again!
204:	      break;

そしてexception_causeで分別されて処理されている。ほとんど無関係な例外や割込では、ドライバ関数disable_mpuを呼び出してMPUを無効化している。まあ、それらには来ないはずだからそれでよい。default:でまとめていないのはEVK1100ボードでLEDを点灯しているためと思われる。
この例題で関係するのは2つのケースだけだ。

254:	    case AVR32_EVBA_OFFSET_DTLB_PROT_W/4:
259:	      ((avr32_gpio_port_t *)&AVR32_GPIO.port[LED3_GPIO >> 5])->ovrc  = 1 << (LED3_GPIO & 0x1F);
266:	      if ( (*((unsigned char *)exception_address) & 0xE0 ) == 0xE0 )
268:	        sp[1] = exception_address + 4;
269:	      else
270:	        // Compact instruction
271:	        sp[1] = exception_address + 2;
272:	      break;

1つ目がDTLB書き込み保護例外だ。説明を省いたが、本関数を呼び出している例外ハンドラではpushm命令でR0〜R12とLR(計14個)をスタックに積んでいる。引数exception_addressには呼び出し側でレジスタspの値に14*4+4を加えた値、つまり例外を引き起こしたレジスタPCの格納アドレスである(pushmの14個とSRそしてPCがスタックに積まれる)。
一方、同様に呼び出し側から渡される引数spは、レジスタSPから14*4を差し引いた、つまりpushmする前のSPが格納されている。sp[0]はスタックに積まれたSR、sp[1]はスタックに積まれたPCである。
結果、exception_addressが指すアドレス値と、sp+1に格納されたアドレス値は一致する。なんだかなあ、引数1つでいいと思うんだけど。
やっていることは、スタックに積まれたPCの値から実行された命令が2バイト長か4バイト長か判定し、そのPCを直後の命令を指すように修正しているのだ。こうして次の命令から実行を再開するのだ。
いくつかの命令を確認したが、確かに4バイト命令はMSB側3bitがセットされている。それならそうとアーキテクチャ・マニュアルに書いてくれればいいのに(どこかに書いてあるのかな?)。

214:	    case AVR32_EVBA_OFFSET_ITLB_PROT/4:
219:	      ((avr32_gpio_port_t *)&AVR32_GPIO.port[LED2_GPIO >> 5])->ovrc  = 1 << (LED2_GPIO & 0x1F);
220:	      disable_mpu();  // Disable the MPU to avoid to trigger the exception again!
221:	      break;

関係するもう1つはITBL保護例外だ。
ここでは、無関係な例外同様、MPUを無効化して終了している。Data系と異なり、Instruction系はスタック情報から再実行とは行かないのでやむを得まい。



MPUによる例外発生は他にもあるが、この例題で扱っているのは上記の2つだけだ。
DTLB書き込み保護例外のリカバリ内容は興味深いが、こんなことしても必要なデータが保存されないことに変わりがなく、復帰できたからといって継続的に正常動作するとは言えないだろう。保護例外処理では、個々のデータの意味など判らず形式的に修復しているだけだ(例題を批判しているのではなく、製品で真似しないでね、という意味)。
ウォッチドッグ・タイマ同様に、異常を検出したらリセットして再起動掛けるのが正道だろう。OSが居るなら、プロセスの再起動でも良い。

ちなみに、実際にMPUドライバの例題を実行したところ、(当たり前だが)問題なく動作した。

*1:U64 *)&__stack_size__)>>10; 453: if(KO == mpu_convert_kbsize_to_eregionsize(&RegionSize, u32kBSizeValue