STM32F4等ではSRAMとCCMRAMが分離されているので、リンカスクリプトもそれに対応したものが出力される(スタートアップルーチンは…… →後述)。
STM32G4ではSRAMとCCMRAMの区別が弱くなり、必要に応じてシームレスに使うことができる。そのため、CubeMXで出力したリンカスクリプトにはCCMRAM領域が含まれていない。
今回、G4474REに搭載されたSRAM1(80K)/SRAM2(16K)/CCMRAM(32K)の3領域を区別して使いたい必要に迫られたので、方法をメモ。
基本的には公式のドキュメントがあるのでそれに従えばいい。
CCM SRAMからプログラムを実行する方法|デザイン/サポート|STM32, STM8ファミリはSTの32bit/8bit汎用マイクロコントローラ製品
リンカスクリプト
MEMORY
RAM2とCCMRAM/CCMRAM2を追加する。
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 80K
RAM2 (rw) : ORIGIN = 0x20014000, LENGTH = 16K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 30K
CCMRAM2 (xr) : ORIGIN = 0x10007800, LENGTH = 2K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}
GCCの場合、1つのセクションに変数と実行コードを配置することはできないらしく、CCMRAM領域にプログラムを配置したい場合は2つに分けて定義する必要がある。今回はとりあえず簡単な動作確認が目的なのでプログラムには2Kを割り当てた。
セクション
} >RAM AT> FLASH
までがデフォルトで書いてある部分。それより下を追加する。
SRAM2の領域をdata2
のような名前にすると、data*
のワイルドカードでマッチしてSRAM1の領域に配置されてしまうので、明確に違う名前を付ける必要がある。
/* Initialized data sections goes into RAM, load LMA copy after code */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH
_siram2 = LOADADDR(.ram2);
.ram2 :
{
. = ALIGN(4);
_sram2 = .;
*(.ram2)
*(.ram2*)
. = ALIGN(4);
_eram2 = .;
} >RAM2 AT> FLASH
_siccmram = LOADADDR(.ccmram);
.ccmram :
{
. = ALIGN(4);
_sccmram = .;
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
_eccmram = .;
} >CCMRAM AT> FLASH
_siccmramex = LOADADDR(.ccmramex);
.ccmramex :
{
. = ALIGN(4);
_sccmramex = .;
*(.exccmram)
*(.exccmram*)
. = ALIGN(4);
_eccmramex = .;
} >CCMRAM2 AT> FLASH
スタートアップルーチン
/* Copy the data segment initializers from flash to SRAM2 */
の上の行までがデフォルトで書いてあるコード。それ以下を追加する。
/* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
CopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit
/* Copy the data segment initializers from flash to SRAM2 */
ldr r0, =_sram2
ldr r1, =_eram2
ldr r2, =_siram2
movs r3, #0
b LoopCopyDataInit2
CopyDataInit2:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit2:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit2
/* Copy the data segment initializers from flash to CCMRAM */
ldr r0, =_sccmram
ldr r1, =_eccmram
ldr r2, =_siccmram
movs r3, #0
b LoopCopyDataInit3
CopyDataInit3:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit3:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit3
/* Copy the data segment initializers from flash to CCMRAM-EX */
ldr r0, =_sccmramex
ldr r1, =_eccmramex
ldr r2, =_siccmramex
movs r3, #0
b LoopCopyDataInit4
CopyDataInit4:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit4:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit4
気になる人はアセンブリ言語を調べてみるとよかろう。低レベル言語だから当然なんだけど、ラベルとgotoでループしたり、レジスタの用途がポンポン変わったり(整数入れたりポインタ入れたり)するので、C言語で擬似コード書いて説明しようとすると大変なことになる。
Cソース
__attribute__
とsection
で配置するセクションを指示する。
変数の場合は宣言の後、初期化の前に指示する。
関数の場合は宣言の後に指示する(定義と同時に指示することはできない)。
uint8_t arr_a[2] __attribute__((section(".data"))) = {0x12, 0x34};
uint8_t arr_b[2] __attribute__((section(".ram2"))) = {0x56, 0x78};
uint8_t arr_c[2] __attribute__((section(".ccmram"))) = {0x9A, 0xBC};
void func_test(void) __attribute__((section(".exccmram")));
void func_test(void)
{
printf(" data %08lX %02hX %02hX\n", reinterpret_cast<uint32_t>(arr_a), arr_a[0], arr_a[1]);
printf(" ram2 %08lX %02hX %02hX\n", reinterpret_cast<uint32_t>(arr_b), arr_b[0], arr_b[1]);
printf("ccmram %08lX %02hX %02hX\n", reinterpret_cast<uint32_t>(arr_c), arr_c[0], arr_c[1]);
printf(" func %08lX\n", reinterpret_cast<uint32_t>(func_test));
}
data 20000004 12 34
ram2 20014000 56 78
ccmram 10004000 9A BC
func 10007801
ちゃんと関数がCCMRAMに配置されて、グローバル変数もそれぞれのセクションに配置され、初期化/実行が可能。
雑記
実行セクション
GCCでデータとプログラムを同じセクションに置けないのは、オブジェクトファイルを作る際の問題。そして、(当然だが)名前が違えば違うセクションとして認識される。
リンクするときには、リンカスクリプトではワイルドカードが指定されているので、ccmram
で始まる名前はすべてccmramに配置される。例えば、データをccmram
、コードをccmramex
みたいな名前で指定しておけば、どちらもccmramに配置されるので、リンカスクリプトやスタートアップルーチンを少し簡単にできる。
ただ、CCMRAMはいくつかのマイコンでは1KB毎に書き込み保護を指定できるので、明確に実行領域を分離しておけば、mainループの最初に(orスタートアップルーチンで書き込んだ直後に)書き込み保護を設定して、プログラム領域を書き換えてしまう危険性をなくすことができる(書き換わる危険は無くせないが)。
書き込み保護はSYSCFG->SWPR = 0xC0000000;
のようにすればCCMRAMの後端2Kに設定できる。
STM32F4のスタートアップルーチン
STM32F4のリンカスクリプトにはCCMRAMの定義が含まれているが、スタートアップルーチンには初期値のコピーが含まれていない。リンカスクリプトに書いてあるとおり、自分でスタートアップルーチンを初期化する必要がある。
// いままで初期値が関係ない用途でしか使ってなかったので気が付かなかった