ルモーリン

MPUはメモリ保護ユニット

投稿:2013-03-07

バグでベクタテーブル(0x00000000から204バイト)本体へジャンプしてテーブルを命令として実行している事が分かりました。 HardFaultで捕捉した時にはどこからジャンプしているか不明です。 そこでジャンプの着地を捕捉してバグ解決への糸口としたい。
NXPのサイトとユーザマニュアル(PDF)。
LPCマイコン情報:LPC1700 Cortex-M3搭載-USB,Ethernet,LCDコントローラ内蔵
UM10360 LPC17xx User manual

ARMのサイトとテクニカルリファレンスマニュアル(PDF)。r2p0を見る事に注意してください。例外発生時のスタック配置はr2p1にありません。
ARM - The Architecture For The Digital World
CortexTM-M3 r2p0 テクニカルリファレンス マニュアル
LPC1769内蔵のメモリ保護ユニットを使ってベクタテーブルの領域から命令フェッチを禁止するように設定、 読み出しの際にはハンドラ内で読み出し前の呼出元のアドレスを取得、そこへ復帰すると開発環境のコールスタックが分かるという訳。
通常のリンク方法では、ベクタテーブルに後続してmain関数とかの普通のプログラムを配置しますので0x000000CC以降に配置しています。 一方、メモリ保護ユニットでの設定範囲は2のn乗の指定なので細かい範囲は面倒です。 例えば、命令フェッチ禁止の範囲を0x00000000~0x000000FFにしてしまうと0x000000CC以降のプログラムを実行しようとしてMemManageFaultが発生してしまいます。 そこでプログラムを0x00000100以降に配置すればトラブルを予防できます。 この件でリンカが出力したマップファイルを眺めると、配置したつもりのCRP(Code Read Protection)の設定がありませんでした。 CRPはユーザマニュアルのページ631「32.6 Code Read Protection (CRP)」にあります。 せっかくなのでCRPの設定位置まで丸ごと命令フェッチ禁止にしましょう。 CRP位置は0x000002FCから4バイトなので命令フェッチ禁止を0x00000000~0x000002FFにすると範囲が2の16乗単位になるので設定する側も簡単な訳です。 後続のプログラムは0x00000300以降に配置するために、リンカスクリプトを書き替えます。 あ、Code Redのコピーライトが<おい。
/*
* GENERATED FILE - DO NOT EDIT
* (C) Code Red Technologies Ltd, 2008-10
* Generated linker script file for LPC1768
* Created from nxp_lpc17_c.ld (vRed Suite 3 (NXP Edition) v3.6 (3 [Build 318] [11/04/2011] ))
* By Red Suite 3 (NXP Edition) v3.6.3 [Build 318] [11/04/2011]  on Sun Sep 16 17:39:42 JST 2012
*/


INCLUDE "TestMulti_Debug_lib.ld"
INCLUDE "TestMulti_Debug_mem.ld"

ENTRY(ResetISR)

SECTIONS
{

	/* MAIN TEXT SECTION */	
	.text : ALIGN(4)
	{
		FILL(0xff)
		KEEP(*(.isr_vector))
		
		/* Global Section Table */
		. = ALIGN(4) ;
		__section_table_start = .;
		__data_section_table = .;
		LONG(LOADADDR(.data));
		LONG(    ADDR(.data)) ;
		LONG(  SIZEOF(.data));
		LONG(LOADADDR(.data_RAM2));
		LONG(    ADDR(.data_RAM2)) ;
		LONG(  SIZEOF(.data_RAM2));
		__data_section_table_end = .;
		__bss_section_table = .;
		LONG(    ADDR(.bss));
		LONG(  SIZEOF(.bss));
		LONG(    ADDR(.bss_RAM2));
		LONG(  SIZEOF(.bss_RAM2));
		__bss_section_table_end = .;
		__section_table_end = . ;
		/* End of Global Section Table */
		
		/* Code Read Protection */
		. = 0x000002FC ; /* or 1FC for LPC2000 */
		KEEP(*(.crp))
		
		/* Memory Protection Unit is monitoring area above. */
		. = 0x00000300;	/* 128 byte x 6 sub-region */

		*(.after_vectors*)
		
		*(.text*)
		*(.rodata .rodata.*)
		. = ALIGN(4);
	} > MFlash512

	/* FUNC_00 SECTION */	
	.func_00 : ALIGN(4)
	{
		__section_func_00_start = .;
		KEEP(*(.func_00))
		__section_func_00_end = .;
		. = ALIGN(4);
		
	} > MFlash512

	/*
	 * for exception handling/unwind - some Newlib functions (in common
	 * with C++ and STDC++) use this.
	 */
	.ARM.extab : ALIGN(4)
	{
		*(.ARM.extab* .gnu.linkonce.armextab.*)
	} > MFlash512
	__exidx_start = .;
	
	.ARM.exidx : ALIGN(4)
	{
		*(.ARM.exidx* .gnu.linkonce.armexidx.*)
	} > MFlash512
	__exidx_end = .;
	
	_etext = .;
		
	
	.data_RAM2 : ALIGN(4)
	{
	   FILL(0xff)
		*(.data.$RAM2*)
		*(.data.$RamAHB32*)
	   . = ALIGN(4) ;
	} > RamAHB32 AT>MFlash512
	
	/* MAIN DATA SECTION */

	.uninit_RESERVED : ALIGN(4)
	{
		KEEP(*(.bss.$RESERVED*))
	} > RamLoc32

	.data : ALIGN(4)
	{
		FILL(0xff)
		_data = .;
		*(vtable)
		*(.data*)
		. = ALIGN(4) ;
		_edata = .;
	} > RamLoc32 AT>MFlash512

	
	.bss_RAM2 : ALIGN(4)
	{
		*(.bss.$RAM2*)
		*(.bss.$RamAHB32*)
	   . = ALIGN(4) ;
	} > RamAHB32

	/* MAIN BSS SECTION */
	.bss : ALIGN(4)
	{
		_bss = .;
		*(.bss*)
		*(COMMON)
		. = ALIGN(4) ;
		_ebss = .;
		PROVIDE(end = .);
	} > RamLoc32
	
	PROVIDE(_pvHeapStart = .);
	PROVIDE(_vStackTop = __top_RamLoc32 - 0);
}
メモリ保護ユニットを設定します。 仕様はユーザマニュアルのページ786「34.4.5 Memory protection unit」にあります。 知らない言語(英語)で記述されている上に輪を掛けて訳わからない仕様なので頭からぷすぷす煙を出しつつ試行錯誤して出来るようになりました。 リージョンの設定は先頭アドレスとサイズで範囲を決めてデフォルトの一部を上書きするイメージです。 リージョンを8等分したサブリージョンについて個々に上書き無効を指定するとデフォルトが復活します。 今回はリージョン0を0x00000000から2048バイトとして、1個のサブリージョンは2048÷8=256バイトとなります。 サブリージョンの1~3個目がデフォルトを上書きして命令フェッチ禁止、4~8個目は上書きを無効にしてデフォルトを復活させました。 メモリ保護ユニット ソースはこんな感じ。
// デフォルトの設定でMPU使用開始
MPU->CTRL = 0b101;

// RBARで一緒に設定するので使わない
// MPU->RNR = 0;

// リージョン0の設定
// フラッシュメモリの先頭から2Kバイトのうち、256バイト×3サブリージョン=768バイト
// 残りの5サブリージョンは禁止=デフォルトの設定
MPU->RBAR = 0x00000000
                | 0x10
                |  0x0;

            //                               E
            //                               N
            //                          SSSSSA
            //          TTT   SSSSSSSS  IIIIIB
            //   X AAA  EEE   RRRRRRRR  ZZZZZL
            //   N PPP  XXXSCBDDDDDDDD  EEEEEE
            //1.987654321.987654321.987654321.
MPU->RASR = 0b00010111000010001111100000010101;
ユーザマニュアルのページ775「34.4.3.10 System Handler Control and State Register」に説明があってビット16「MEMFAULTENA」を1にすると有効になります。 ソースはこんな感じ。
// MEMFAULTENA
SCB->SHCSR |= 0b1 << 16;
命令フェッチ禁止の範囲でどこをフェッチしようとしたか、どこからそこに飛んできたかを把握したい。 そこでハンドラに到着してからそれらの情報を手に入れます。 ユーザマニュアルのページ777「34.4.3.11 Configurable Fault Status Register」にあるMMFSRのビット7「MMARVALID」を見ると Fault発生時に発生アドレスを記録しているか分かります。 記録した場合はページ781「34.4.3.13 Memory Management Fault Address Register」にあります。 それとは別にARMが配布している「Cortex-M3 テクニカルリファレンスマニュアル」ページ5-11(111)「5.5.1スタック操作」によると、 発生時のPCとLRがスタックに積まれていますのでこれを参照します。 プロセススタックとメインスタックのどちらに積んだのかユーザマニュアルのページ734「34.3.1.3.7 CONTROL register」にあるビット1「Active stack pointer」で判定します。 もしメインスタックに積んでいる場合は、到着したハンドラもスタックを使ってSPを移動させていますからそれを相殺します。 相殺量を開発環境の逆アセンブラでハンドラ入口のSP変更量を見てからソースをいじりました(笑)。 関数入口到着時点のSPを関数内部で拾う方法があればそっちのほうがいいです。というかそうしたい。 ソースはこんな感じ。
// 発生アドレス保持?
if (0x80 & SCB->CFSR)
{
        uint32_t pc = SCB->MMFAR;
        pc = pc;
}

typedef void(*I_ADDR)(void);
I_ADDR *pStack;
// プロセススタック使用中にMemManageFault発生
if (0x02 & __get_CONTROL())
{
        pStack = (I_ADDR *)__get_PSP();
}
else
{
        // メインスタック使用中にMemManageFault発生
        pStack = (I_ADDR *)__get_MSP();
        // この関数もメインスタックを使用していて入口で動いた分を相殺
        pStack += 6;
}
I_ADDR lr = pStack[5];
lr = lr;
I_ADDR pc = pStack[6];
pc = pc;
スタックに積まれたLRは呼出元のアドレスなので、通常は処理後にLRに戻ります。 けれどFaultが発生しているので戻ってくれませんから、呼出元に濡れ衣を着せてそこで発生したように細工して戻します。 冒頭の判定は、ユーザマニュアルのページ777「34.4.3.11 Configurable Fault Status Register」にあるMMFSRのビット0「IACCVIOL」で 命令フェッチ違反を判定しています。
// IACCVIOL
if (0x01 & SCB->CFSR)
{
        pStack[6] = lr;	// フォルトは発生PCに戻るので、呼出元に戻るよう細工する
}
  • メモリ保護ユニットの略称がMemory Protection Unit = MPUでは、GoogleらなくてもMicro Processing Unitと一緒になって検索の役に立たないのは明らか。
  • メモリ保護ユニットの設定に違反する動作をCPUが行った場合はMemManageFaultになります。 ハンドラにブレークポイントを設定して故意に違反させてみたが何故かHardFaultになる。 例によってユーザマニュアルを穴があくほど読むとMemManageFaultは有効/無効の制御ができるようになっていて初期値は無効。 MemManageFaultに相当するイベントが発生した時にそれが無効であればHardFaultとなります。
  • スケルトンのプロジェクトで専用のハンドラ関数を生成しているにも関わらずFaultを有効にするコードが生成されません>RedSuite3(NXP版)。