背景
現在、USB-MIDIインターフェースをSTM32F407で作っています。
これは単なるUSB-MIDIへの変換だけでなく、MIDI to MIDIへのルーティングやイベントフィルタ、チャンネル・マッピング、そのほか多くの機能を提供するものに仕上げる予定です。
これらの機能を実現するため、
- MIDIで受信したデータを保持
- 受信データの演算
- 送信先への振り分け
などの処理を行いますが、これを行うには一時的なメモリがKバイトオーダーで必要になります。
今まで何も考えなければ、必要時スタックに演算用の領域を確保し、終わったら解放...mallocを使わなければこんな感じ。
mallocは多用すると使用可能領域が断片化するかもしれないので、基本的には使わないようにしています。
スタックを使えば特に問題ないですが、STM32F407にはCCMRAMという64Kバイトのメモリが0x1000_0000番地に配置されています。
普通にソフトを書いていると、ここは使われずに放置されています。
であれば、演算用のメモリとして使えば、内蔵SRAMをもっと他のことに使える!
ということで、CCMRAMを使うことを考えます。
STM32F407のメモリマップ
STM32CubeMXで生成されるリンカマップを見てみると、以下のようになっています。
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
内蔵SRAMが128Kバイト、CCMRAMが64Kバイトあります。
普通にソフトを書いていると、RAMの128Kバイトに変数などが割り当てられ、CCMRAMは使われません。
CCMRAMを使う方法
変数に直接アドレスを指定する
CCMRAMが0x1000_0000番地から割り当てられていることはわかっています。
だったら、直接アドレスを指定して使うことができます。
*((unsigned char *)0x10000000) = 0x55;
*((unsigned short *)0x10000010) = 0x55aa;
#define PATTERN_ADDR 0x1000000040
*((char *)PATTERN_ADDR) = 0x11;
これが一番簡単です。
でも、アドレスの管理を自分でしなくてはいけませんね。
これは大変ですが、変数が少なければこれでもいいかも。
汎用性はないですが、とにかく簡単です。
変数を構造体にまとめて構造体をアドレス指定
直接アドレスを指定するのは変わりませんが、CCMRAMで使いたい変数などを構造体にまとめてしまいます。
typedef struct CCMRAM_VARS {
unsigned char leftData;
unsigned short rightArign;
char onPattern[16];
}CCMRAM_VARS;
((CCMRAM_VARS *)0x10000000)->leftData = 0x55;
CCMRAMのアドレスを指定するのは同じですが、変数のアドレス管理はしなくて良くなります。
常に構造体メンバの名前でアクセスできます。
大きなシステムではよく使っていました。
複数のCPUが並列動作するシステムでは、丸ごとコピーを渡したりするのに便利だし、メモリの管理もコンパイラに任せられます。
CCMRAMのセクションを作ってリンカに任せる
この方法は一番柔軟性があります。
通常通り変数を生成して、それをRAMではなくCCMRAMのセクションに割り当てればいいだけです。
CCMRAMへのアドレス割り当てはリンカがやってくれます。
初期値付きの変数もCCMRAMへ配置できます。
この方法を利用するには、
- リンカスクリプト
- スタートアッププログラム(アセンブラ)
- 変数宣言時にセクションを指定する
という手続きが必要です。
自分の備忘録として、この方法を解説します。
リンカスクリプトの修正
調べるまで気づかなかったのですが、実はCCMRAMを使うための下記記述がリンカスクリプトに記述されていました。
_siccmram = LOADADDR(.ccmram);
/* CCM-RAM section
*
* IMPORTANT NOTE!
* If initialized variables will be placed in this section,
* the startup code needs to be modified to copy the init-values.
*/
.ccmram :
{
. = ALIGN(4);
_sccmram = .; /* create a global symbol at ccmram start */
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
_eccmram = .; /* create a global symbol at ccmram end */
} >CCMRAM AT> FLASH
ccmramセクションの構造を見ると、初期値付き変数のセクションです。
初期値の記述先(ロードアドレス)を_siccmram = LOADADDR(.ccmram)で求めています。
じゃ、ccmramセクションに配置すればすぐ使えるのか...と思うのですが、IMPORTANT NOTE!を見ると、このセクションは初期化されないので、スタートアップコードを修正しなさい...と書かれてます。
ということで、初期値付き変数セクションはこのままccmramセクションを利用します。
初期値ゼロの変数セクションを作る
いわゆるbssと同じセクションをCCMRAM上に配置します。
セクション名はbssに似せてbbbにしました。
/**** 初期値0のCCMRAM変数セクションを追加 ****/
. = ALIGN(4);
.bbb :
{
_sbbb = .; /* define a global symbol at bbb start */
*(.bbb)
*(.bbb*)
. = ALIGN(4);
_ebbb = .; /* define a global symbol at bbb end */
} >CCMRAM
スタートアップのプログラムでゼロフィルするために、_sbbbと_ebbbというグローバルシンボルを用意します。
bbbセクションのスタートとエンドのアドレスです。
この範囲をスタートアッププログラムでゼロフィルします。
スタートアッププログラムを修正する
スタートアッププログラムはアセンブラで記述されています。
主にRAMのゼロフィル、初期値のロードが行われています。
ここに、新しく作ったccmram、bbbセクションの初期化を追記します。
初期値付き変数のccmramセクションを初期化する
まずは通常の初期値付き変数(dataセクション)の初期化を見てみます。
dataセクションの
- スタートアドレスsdata
- エンドアドレスedadta
- 初期値の配置アドレスsidata
を使って、内蔵Flashから内蔵SRAMへ初期値をコピーしています。
リンカスクリプトでアラインメントを4にしているので、値のコピーは4バイトまとめてやってますね。
/* 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 CCMRAM ****/
ldr r0, =_sccmram
ldr r1, =_eccmram
ldr r2, =_siccmram
movs r3, #0
b LoopCopyccmramInit
CopyccmramInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyccmramInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyccmramInit
初期値0変数のbbbセクションを初期化する
同じくbssセクションのゼロフィル処理を見てみましょう。
/* Zero fill the bss segment. */
ldr r2, =_sbss
ldr r4, =_ebss
movs r3, #0
b LoopFillZerobss
FillZerobss:
str r3, [r2]
adds r2, r2, #4
LoopFillZerobss:
cmp r2, r4
bcc FillZerobss
- スタートアドレスsbss
- エンドアドレスebss
を使って、指定アドレスを0で埋めています。
dataセクションと同じく、リンカスクリプトでアラインメントを4にしているので、4バイトまとめて0を入れてます。
同じように、これをそのまま使わせてもらいましょう。
ccmramセクション同様、分岐ラベルは適切な名前を付けます。
/**** Zero fill the CCMRAM segment. ****/
ldr r2, =_sbbb
ldr r4, =_ebbb
movs r3, #0
b LoopFillZeroccmram
FillZeroccmram:
str r3, [r2]
adds r2, r2, #4
LoopFillZeroccmram:
cmp r2, r4
bcc FillZeroccmram
これでCCMRAMもSRAMと同じように扱えます。
CのソースコードでCCMRAMを使う
こんな感じで書けば使えます。
__attribute__((section(".bbb"))) PROCESSING_DATA processingData;
__attribute__((section(".ccmram"))) unsigned char demoData[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
動作を見てみる
ちゃんとゼロフィル、初期値ロードがされていますね。
最後に
今までそこそこSRAMが足りていたので気にしてませんでしたが、64Kバイトはデカい!
アプリケーションによっては使ったほうがいい時もあります。
CCMRAMを検討している方の参考になれば幸いです。
あと、この手法はChatGPTでも紹介してくれますよ♪
