STM32CubeMXのリンカスクリプトにCCMRAM領域を追加する

STM32F4等ではSRAMとCCMRAMが分離されているので、リンカスクリプトもそれに対応したものが出力される(スタートアップルーチンは…… →後述)。
STM32G4ではSRAMとCCMRAMの区別が弱くなり、必要に応じてシームレスに使うことができる。そのため、CubeMXで出力したリンカスクリプトにはCCMRAM領域が含まれていない。

今回、G4474REに搭載されたSRAM1(80K)/SRAM2(16K)/CCMRAM(32K)の3領域を区別して使いたい必要に迫られたので、方法をメモ。

基本的には公式のドキュメントがあるのでそれに従えばいい。

[CCM SRAMからプログラムを実行する方法｜デザイン/サポート｜STM32, STM8ファミリはSTの32bit／8bit汎用マイクロコントローラ製品](https://www.stmcu.jp/design/document/technical_note/77109/)

## リンカスクリプト

### `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`で配置するセクションを指示する。
変数の場合は宣言の後、初期化の前に指示する。
関数の場合は宣言の後に指示する(定義と同時に指示することはできない)。

```c
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の定義が含まれているが、スタートアップルーチンには初期値のコピーが含まれていない。リンカスクリプトに書いてあるとおり、自分でスタートアップルーチンを初期化する必要がある。
// いままで初期値が関係ない用途でしか使ってなかったので気が付かなかった