春雨日記 about me tags

STM32でPSRAMをメモリマップド動作させるメモ

はじめに

STM32(MPシリーズを除く)で画像などの大容量コンテンツを扱おうとしたとき、真っ先に問題になるのがメモリ不足だと思います。

ハイエンドなH7シリーズでも内臓SRAMは最大2MB程度であり、画像を多く扱うようなシーンでは節約しても焼け石に水な状況となってしまいます。

現在STM32で地理院タイルを大量に並べて表示させる事を画策しているのですが、やはり問題になるのはメモリ不足でした。

STM32H7シリーズはFMCなどのメモリを拡張する術をいくつか持っており、その中からQSPI接続のPSRAMを使用してみる事にしました。

環境

ESP32シリーズでPSRAMが使用できるのは知っていましたが、ばら売りしている事は知らなかったので驚きました。 データシートも公開されており、使用する上で不自由は無かったです。

ただ、英語の物は図が潰れて読みにくいので中国語の方を見る事をおすすめします。

ちなみに、使用したマイコンボードにはQSPI FlashとしてWinbondのW25Q64JVが付いていましたが、ESP-PSRAM64Hとピン配置が同じだったのでそのまま載せ替えました。

ESP32以外にこれが載ってる姿は新鮮です(笑)

CubeMX設定

QSPIについての解説は、NORフラッシュを扱っているこちらのブログが大変わかりやすかったです。

Flash Sizeは、2^(N+1) [bytes]と計算するらしいです。ESP-PSRAM64Hは8MBなので22にしました。

加えて、境界アクセスをする(デフォルト動作)際はクロックを84MHzまでに抑える必要があるので、プリスケーラやクロック設定でいい感じにして下さい。(データシートのp4あたり参照)

MPUが有効でメモリマップドモードを使いたい場合はリージョンの設定も忘れないでください。

D-Cacheが有効な場合はパラメータを煮詰める必要があるのですが、無効ならアドレスと許可ぐらいで大丈夫です。

許可が無い場合、アクセスした場合にフォールトが発生します。

普通に読み書き

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
uint8_t SRAM_Init(){
	uint8_t ret = 0;
	uint8_t receive[8];
	QSPI_CommandTypeDef qcmd = {0};

	//exit qspi
	qcmd.Instruction = 0xf5;
	qcmd.InstructionMode = QSPI_INSTRUCTION_4_LINES;
	qcmd.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;
	qcmd.DdrMode = QSPI_DDR_MODE_DISABLE;
	qcmd.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
	qcmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;

	HAL_QSPI_Command(&hqspi, &qcmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
	HAL_Delay(100);

	//reset
	qcmd.Instruction = 0x66;
	qcmd.InstructionMode = QSPI_INSTRUCTION_1_LINE;
	HAL_QSPI_Command(&hqspi, &qcmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
	HAL_Delay(100);
	qcmd.Instruction = 0x99;
	HAL_QSPI_Command(&hqspi, &qcmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
	HAL_Delay(100);

	//check ID
	qcmd.Instruction = 0x9f;
	qcmd.AddressMode = QSPI_ADDRESS_1_LINE;
	qcmd.AddressSize = QSPI_ADDRESS_24_BITS;
	qcmd.Address = 0;
	qcmd.DataMode = QSPI_DATA_1_LINE;
	qcmd.NbData = 8;
	HAL_QSPI_Command(&hqspi, &qcmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
	HAL_QSPI_Receive(&hqspi, receive, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);

	if(receive[0] != 0x0D)
		ret |= 0x1;
	if(receive[1] != 0x5D)
		ret |= 0x2;

	//enable QSPI
	qcmd.Instruction = 0x35;
	qcmd.AddressMode = QSPI_ADDRESS_NONE;
	qcmd.DataMode = QSPI_DATA_NONE;
	qcmd.NbData = 0;
	HAL_QSPI_Command(&hqspi, &qcmd, 100);
	return ret;
}

void SRAM_Write(uint32_t addr, uint8_t* data, uint32_t len){
	QSPI_CommandTypeDef qcmd = {0};
	qcmd.Instruction = 0x38;
	qcmd.InstructionMode = QSPI_INSTRUCTION_4_LINES;
	qcmd.Address = addr;
	qcmd.AddressMode = QSPI_ADDRESS_4_LINES;
	qcmd.AddressSize = QSPI_ADDRESS_24_BITS;
	qcmd.DataMode = QSPI_DATA_4_LINES;
	qcmd.NbData = len;
	qcmd.SIOOMode = QSPI_SIOO_INST_ONLY_FIRST_CMD;
	qcmd.DdrMode = QSPI_DDR_MODE_DISABLE;
	qcmd.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
	qcmd.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
	HAL_QSPI_Command(&hqspi, &qcmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
	HAL_QSPI_Transmit(&hqspi, data, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
}

void SRAM_Read(uint32_t addr, uint8_t* data,uint32_t len){
	QSPI_CommandTypeDef qcmd = {0};
	qcmd.Instruction = 0xeb;
	qcmd.InstructionMode = QSPI_INSTRUCTION_4_LINES;
	qcmd.Address = addr;
	qcmd.AddressMode = QSPI_ADDRESS_4_LINES;
	qcmd.AddressSize = QSPI_ADDRESS_24_BITS;
	qcmd.DataMode = QSPI_DATA_4_LINES;
	qcmd.NbData = len;
	qcmd.DummyCycles = 6;
	HAL_QSPI_Command(&hqspi, &qcmd, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
	HAL_QSPI_Receive(&hqspi, data, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);
}

こういう関数を作る事で、関数から自由に書き込めるようになります。 HAL_QSPI_Transmit_ITにすると割り込みモードでも動きます。

本当はDMAでやりたかったのですが、色々触ってもちゃんと動かなかったので断念しました。 結局はメモリマップドモード(後述)で使用する予定なので、ここは深追いしないでおきます。

メモリマップドモード

STM32のQSPIペリフェラルでは、読み込み専用ですがメモリマップドモード動作によってまるでSRAM上にあるかのようにアクセスできます。

何が嬉しいかというと、DMA2Dからアクセス可能になるのでQSPI接続のSRAMを画像バッファとして使えるという事です。

書き込む際にはメモリマップドモードを解除した上で上記の関数などを用いる必要がありますが、私の用途では読み込みの回数が圧倒的に多そうなので問題ないと考えています。

ちなみに、最近のSTM32シリーズに搭載されているOSPI(8線シリアル)では書き込みもできるらしいです。シリアルなのに8本て…(謎)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void SRAM_StartMMM(){
	QSPI_CommandTypeDef qcmd = {0};
	QSPI_MemoryMappedTypeDef qcfg = {0};
	qcmd.Instruction = 0xeb;
	qcmd.InstructionMode = QSPI_INSTRUCTION_4_LINES;
	qcmd.AddressMode = QSPI_ADDRESS_4_LINES;
	qcmd.AddressSize = QSPI_ADDRESS_24_BITS;
	qcmd.DataMode = QSPI_DATA_4_LINES;
	qcmd.DummyCycles = 6;

	qcfg.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE;
	HAL_QSPI_MemoryMapped(&hqspi, &qcmd, &qcfg);
}

void SRAM_StopMMM(){
	HAL_QSPI_Abort(&hqspi);
}

SRAM_StartMMM()した状態で、0x90000000にアクセスすると内容が見えます。

使用例

上記の関数はこんな感じに使えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(void){
	// 省略

	//JPEG画像をデコード(この記事とは無関係)
	hwjpeg_init(&hjpeg);
	hwjpeg_start(&jpegfile, (uint8_t*)jpgbuf_out);
	hwjpeg_status_t stat;
	while((stat = hwjpeg_process()) != HWJPEG_OK){
		if(stat & HWJPEG_ERROR){
			while(1);
		}
	}
	JPEG_ConfTypeDef jpgConf;
	HAL_JPEG_GetInfo(&hjpeg, &jpgConf);

	//SRAMを初期化
	SRAM_Init();
	//SRAMの8番地にデコードされた画像を書き込み
	SRAM_Write(8, (void*)jpgbuf_out, 256*256*2UL);
	//メモリマップドモード開始
	SRAM_StartMMM();

	//DMA2DでQSPI SRAMの8番地からフレームバッファに画像を転送
	DMA2D_Transfer((uint8_t*)(0x90000008),(uint8_t*) ltdc_layer0 , 0, 0, jpgConf.ImageWidth, jpgConf.ImageHeight);
}

おわりに

画像を扱おうとすると一瞬でメモリが奪われますが(?)、簡単に拡張できる方法がわかって安心です。

QSPIペリフェラルでは最大2デバイスを載せられるようなので、2個載せて128Mbitにしてもいいかもしれないですね。

ちなみに、使用例の画像は所謂PSP液晶(のコンパチ)をLTDCで使っています。800円ぐらいで買えてこの綺麗さは優秀過ぎる…