STM32L5を使ってみる3 -OCTO-SPIでQUAD-SPIROMを使おう-

●QUAD-SPI接続NORFLASH-ROMを使いこなせ!
FONTX2ファイルなどの大容量のデータはSTM32等の大規模マイコンと
いえどもフラッシュ容量を非常にひっ迫します。
以前からはそういったデータは外付けのQSPI-ROMに格納し、そこから
アクセスするといった手法が定番となっております。

STM32L5では信号線を最大8本持つクロックドシリアルインターフェース
いわゆる"OCTO-SPI"をもちさらに利便性が高まっております!
このOCTO-SPIを使ってQSPI-ROMと接続し内蔵フラッシュメモリのように
使えるMemoryMappedModeで読み出していこうと思います!


●もはや定番☆WinbondのW25Q128JVSIQ

前々回からの紹介の通り、ねむいさんは入手も容易で十分な容量(16MB)、
高速なクロック(133MHz)でデフォルトでQUAD-SPIモードになっていて
コマンドが省略出来て扱いやすいW25Q128JVSIQを使用しました。

W25Qのシリーズにはいろいろありますが末尾"IQ"のものは上でも
書いた通り最初っからQUAD-SPIモードになっており今から紹介する
メモリマップドモードで投げるコマンドがいくつか省略出来て楽です。
digikeyやmouserで購入の際はうっかり違うやつを買わないように
型番には注意してください。


●OCTO-SPIをMemoryMappedModeで使用するためのコード手順
STM32L5に搭載されているOCTO-SPIはSTM32F7などにあったQUAD-SPI
インターフェースからされに拡張され、読み出しのほかに書き込みも
サポートされています。基本的な設定の流れはQUAD-SPIの頃とほぼ
同じですがOCTO-SPIならではの設定もあるので注意しましょう。

↓まずはI/O設定です。

/**************************************************************************/
/*!
@brief OCTO-SPI GPIO Configuration.
@param None.
@retval None.
*/
/**************************************************************************/
void OSPI_IoInit_If(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

/* Initializes the peripherals clock */
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_OSPI;
PeriphClkInit.OspiClockSelection = RCC_OSPICLKSOURCE_SYSCLK;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
for(;;);
}

/* Peripheral clock enable */
__HAL_RCC_OSPI1_CLK_ENABLE();

/* Alternate GPIO enable */
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();

/* PE12 as OSPI_IO0 */
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF10_OCTOSPI1;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

/* PB0 as OSPI_IO1 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

/* PE14 as OSPI_IO2 */
GPIO_InitStruct.Pin = GPIO_PIN_14;
GPIO_InitStruct.Alternate = GPIO_AF10_OCTOSPI1;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

/* PE15 as OSPI_IO3 */
GPIO_InitStruct.Pin = GPIO_PIN_15;
GPIO_InitStruct.Alternate = GPIO_AF10_OCTOSPI1;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);

/* PB10 as OSPI_CLK */
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Alternate = GPIO_AF10_OCTOSPI1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

/* PA2 as OSPI_NCS */
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Alternate = GPIO_AF10_OCTOSPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

これはNUCLEO-L552ZE-Qから出たQUAD-SPIの足に対応したものです。
OCTO-SPIなのに8本全部使わぬぇのかと思いでしょうがこれはNUCLEO
が引き出してる足がそうなってるからそれに従ったわけなのでー

↓お次はOCTO-SPIの設定です。
/**************************************************************************/
/*!
@brief Configure OCTO-SPI as Memory Mapped Mode.
Winbond W25Q128JVSIQ specific setting.
* Address size is 24bit(16MBytes).
* Initially sets QUAD-MODE(not need quadmode command).
* MAX 133MHz CLK.
* Support "XIP",thus suitable for MemoryMappedMode.
@param None.
@retval None.
*/
/**************************************************************************/
void Set_OSPI_MemoryMappedMode(void)
{
OSPI_RegularCmdTypeDef sCommand = {0};
OSPI_MemoryMappedTypeDef sMemMappedCfg = {0};
uint8_t reg_data =0;

/* Initialize OCTO-SPI I/O */
OSPI_IoInit_If();

/* Initialize OCTO-SPI */
hospi.Instance = OCTOSPI1;
hospi.Init.FifoThreshold = 1;
hospi.Init.DualQuad = HAL_OSPI_DUALQUAD_DISABLE;
hospi.Init.MemoryType = HAL_OSPI_MEMTYPE_MICRON;
hospi.Init.DeviceSize = 24; /* 128Mbit=16MByte=2^24 W25Q128JVSIQ */
hospi.Init.ChipSelectHighTime = 2; /* 2ClockCycle(18nSec@110MHz) Need for W25Q128JVSIQ >10nSec@read */
hospi.Init.FreeRunningClock = HAL_OSPI_FREERUNCLK_DISABLE;
hospi.Init.ClockMode = HAL_OSPI_CLOCK_MODE_0;
hospi.Init.WrapSize = HAL_OSPI_WRAP_NOT_SUPPORTED;
hospi.Init.ClockPrescaler = 2; /* 110MHzMAX/2 = 55MHz(MAX OSPI-CLK:90MHz) */
hospi.Init.SampleShifting = HAL_OSPI_SAMPLE_SHIFTING_HALFCYCLE;
hospi.Init.DelayHoldQuarterCycle = HAL_OSPI_DHQC_DISABLE;
hospi.Init.ChipSelectBoundary = 0;
hospi.Init.DelayBlockBypass = HAL_OSPI_DELAY_BLOCK_BYPASSED;
hospi.Init.Refresh = 0;
if (HAL_OSPI_Init(&hospi) != HAL_OK)
{
for(;;);
}

/* Enable Reset --------------------------- */
/* Common Commands */
sCommand.OperationType = HAL_OSPI_OPTYPE_COMMON_CFG;
sCommand.FlashId = HAL_OSPI_FLASH_ID_1;
sCommand.InstructionDtrMode = HAL_OSPI_INSTRUCTION_DTR_DISABLE;
sCommand.AddressDtrMode = HAL_OSPI_ADDRESS_DTR_DISABLE;
sCommand.DataDtrMode = HAL_OSPI_DATA_DTR_DISABLE;
sCommand.DQSMode = HAL_OSPI_DQS_DISABLE;
sCommand.SIOOMode = HAL_OSPI_SIOO_INST_EVERY_CMD;
sCommand.AlternateBytesMode = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytes = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesSize = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesDtrMode = HAL_OSPI_ALTERNATE_BYTES_DTR_DISABLE;
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.InstructionSize = HAL_OSPI_INSTRUCTION_8_BITS;
sCommand.AddressSize = HAL_OSPI_ADDRESS_24_BITS;
/* Instruction */
sCommand.Instruction = 0x66; /* Reset Enable W25Q128JVSIQ */
/* Address */
sCommand.AddressMode = HAL_OSPI_ADDRESS_NONE;
sCommand.Address = 0;
/* Data */
sCommand.DataMode = HAL_OSPI_DATA_NONE;
sCommand.DummyCycles = 0;
sCommand.NbData = 0;

if (HAL_OSPI_Command(&hospi, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
for(;;);
}

/* Reset Device --------------------------- */
/* Common Commands */
sCommand.OperationType = HAL_OSPI_OPTYPE_COMMON_CFG;
sCommand.FlashId = HAL_OSPI_FLASH_ID_1;
sCommand.InstructionDtrMode = HAL_OSPI_INSTRUCTION_DTR_DISABLE;
sCommand.AddressDtrMode = HAL_OSPI_ADDRESS_DTR_DISABLE;
sCommand.DataDtrMode = HAL_OSPI_DATA_DTR_DISABLE;
sCommand.DQSMode = HAL_OSPI_DQS_DISABLE;
sCommand.SIOOMode = HAL_OSPI_SIOO_INST_EVERY_CMD;
sCommand.AlternateBytesMode = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytes = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesSize = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesDtrMode = HAL_OSPI_ALTERNATE_BYTES_DTR_DISABLE;
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.InstructionSize = HAL_OSPI_INSTRUCTION_8_BITS;
sCommand.AddressSize = HAL_OSPI_ADDRESS_24_BITS;
/* Instruction */
sCommand.Instruction = 0x99; /* Reset W25Q128JVSIQ */
/* Address */
sCommand.AddressMode = HAL_OSPI_ADDRESS_NONE;
sCommand.Address = 0;
/* Data */
sCommand.DataMode = HAL_OSPI_DATA_NONE;
sCommand.DummyCycles = 0;
sCommand.NbData = 0;

if (HAL_OSPI_Command(&hospi, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
for(;;);
}

/* Enter Quad-SPI Mode --------------------------- */
/* Common Commands */
sCommand.OperationType = HAL_OSPI_OPTYPE_COMMON_CFG;
sCommand.FlashId = HAL_OSPI_FLASH_ID_1;
sCommand.InstructionDtrMode = HAL_OSPI_INSTRUCTION_DTR_DISABLE;
sCommand.AddressDtrMode = HAL_OSPI_ADDRESS_DTR_DISABLE;
sCommand.DataDtrMode = HAL_OSPI_DATA_DTR_DISABLE;
sCommand.DQSMode = HAL_OSPI_DQS_DISABLE;
sCommand.SIOOMode = HAL_OSPI_SIOO_INST_EVERY_CMD;
sCommand.AlternateBytesMode = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytes = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesSize = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesDtrMode = HAL_OSPI_ALTERNATE_BYTES_DTR_DISABLE;
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.InstructionSize = HAL_OSPI_INSTRUCTION_8_BITS;
sCommand.AddressSize = HAL_OSPI_ADDRESS_24_BITS;
/* Instruction */
sCommand.Instruction = 0x31; /* Set Status2 W25Q128JVSIQ */
/* Address */
sCommand.AddressMode = HAL_OSPI_ADDRESS_NONE;
sCommand.Address = 0;
/* Data */
sCommand.DataMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.DummyCycles = 0;
sCommand.NbData = 1;
reg_data = 0x02; /* Enable QuadI/O Mode */

if (HAL_OSPI_Command(&hospi, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
for(;;);
}

if (HAL_OSPI_Transmit(&hospi, &reg_data, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
for(;;);
}

/* Enter MemoryMappedMode --------------------------- */
/* Read Commands */
sCommand.OperationType = HAL_OSPI_OPTYPE_READ_CFG;
sCommand.FlashId = HAL_OSPI_FLASH_ID_1;
sCommand.InstructionDtrMode = HAL_OSPI_INSTRUCTION_DTR_DISABLE;
sCommand.AddressDtrMode = HAL_OSPI_ADDRESS_DTR_DISABLE;
sCommand.DataDtrMode = HAL_OSPI_DATA_DTR_DISABLE;
sCommand.DQSMode = HAL_OSPI_DQS_DISABLE;
sCommand.SIOOMode = HAL_OSPI_SIOO_INST_EVERY_CMD;
sCommand.AlternateBytesMode = HAL_OSPI_ALTERNATE_BYTES_4_LINES;
sCommand.AlternateBytes = 0xFF; /* Need for Fast Read QUAD W25Q128JVSIQ */
sCommand.AlternateBytesSize = HAL_OSPI_ALTERNATE_BYTES_8_BITS;
sCommand.AlternateBytesDtrMode = HAL_OSPI_ALTERNATE_BYTES_DTR_DISABLE;
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.InstructionSize = HAL_OSPI_INSTRUCTION_8_BITS;
sCommand.AddressSize = HAL_OSPI_ADDRESS_24_BITS;
/* Instruction */
sCommand.Instruction = 0xEB; /* Fast Read QUAD W25Q128JVSIQ */
/* Address */
sCommand.AddressMode = HAL_OSPI_ADDRESS_4_LINES;
sCommand.Address = 0;
/* Data */
sCommand.DataMode = HAL_OSPI_DATA_4_LINES;
sCommand.DummyCycles = 4; /* DUMMY 4Cycle for Fast Read QUAD W25Q128JVSIQ */
sCommand.NbData = 0;

if(HAL_OSPI_Command(&hospi, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
for(;;);
}

/* Write Commands */
sCommand.OperationType = HAL_OSPI_OPTYPE_WRITE_CFG;
sCommand.FlashId = HAL_OSPI_FLASH_ID_1;
sCommand.InstructionDtrMode = HAL_OSPI_INSTRUCTION_DTR_DISABLE;
sCommand.AddressDtrMode = HAL_OSPI_ADDRESS_DTR_DISABLE;
sCommand.DataDtrMode = HAL_OSPI_DATA_DTR_DISABLE;
sCommand.DQSMode = HAL_OSPI_DQS_DISABLE;
sCommand.SIOOMode = HAL_OSPI_SIOO_INST_EVERY_CMD;
sCommand.AlternateBytesMode = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytes = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesSize = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.AlternateBytesDtrMode = HAL_OSPI_ALTERNATE_BYTES_DTR_DISABLE;
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.InstructionSize = HAL_OSPI_INSTRUCTION_8_BITS;
sCommand.AddressSize = HAL_OSPI_ADDRESS_24_BITS;
/* Instruction */
sCommand.Instruction = 0x32; /* Page Write QUAD W25Q128JVSIQ */
/* Address */
sCommand.AddressMode = HAL_OSPI_ADDRESS_1_LINE;
sCommand.Address = 0;
/* Data */
sCommand.DataMode = HAL_OSPI_DATA_4_LINES;
sCommand.DummyCycles = 0;
sCommand.NbData = 0;

if(HAL_OSPI_Command(&hospi, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
{
for(;;);
}

/* Set OCTO-SPI as MemoryMappedMode */
sMemMappedCfg.TimeOutActivation = HAL_OSPI_TIMEOUT_COUNTER_DISABLE;
sMemMappedCfg.TimeOutPeriod = 0;
if(HAL_OSPI_MemoryMapped(&hospi, &sMemMappedCfg) != HAL_OK)
{
for(;;);
}

}

流れとしてはリセット有効->リセット->クアッドI/Oモード有効->
クアッド高速読み出しコマンド->クアッドセクタ書き込みコマンド
->HALのMemoryMappedMode移行関数呼び出し
となっております。

"クアッドI/Oモード有効"はW25Q128JVSIQ使うなら本来不要ですが
"念のため"突っ込んどきました…一応なくても動作します。

注意点ですがOCTO-SPIは書き込みもMemoryMappedModeが使用可能の為、
クアッド読み出しコマンドの次にセクタ書き込みコマンドの設定が
必須になります!!


なぜそうなっているかというとHALの関数内で書き込みコマンドの
設定まで要求してきやがるからです(別に今回書き込み要らんのに☠)
これも設定しないと先に進めません!

●動作してるところ

MemoryMappedModeで設定したアドレスはSTM32F7のでやった時と同じく
0x90000000からとなりますので過去のリソースが利用できます。
ねむいさんの使い方ではFONTX2ファイルのAnkの方を0x90000000、
Sjis-Kanjiフォントの方は0x90010000から参照しております。
Ankのファイルサイズはどんなにでかくとも65536byte以上にならない
だろうという打算でこのオフセット値を設定してます。


こんな感じでFONTX2(全角&半角)ファイルを仕込んだW25Q128JVSIQから
内蔵フラッシュと変わらない感覚でデータを読み出し表示ができました。
使用したフォントは小夏フォントを12ptのFONTX2に変換したものです。
STM32L5のOCTO-SPIは命令キャッシュだけでデータキャッシュは無い
ですが文字表示や操作感に特にもたついた感じはしてませんね。



デバッガで0x90000000番地を表示してみました。
MemoyMappedMode有効後は内蔵フラッシュと同じようにデバッガで
内部のデータを参照できてますね。



●ところでどうやってQSPI-ROMにデータ書くの?
…今回の記事はこれが最大の難関です…!
OpenOCDではDiscovery系のボードではI/Fの足もQSPI-ROMの品種も
決め打ちのためにQSPI-ROMの読み書きがサポートされていますが
NUCLEO系のやつは足は出ているもののその先に何がつながるかは
わからないのでサポートしているものは何にもありません。

したがってOpenOCDのmmwのコマンドでそれぞれのROMデバイス向けに
上で書いたコードを16進数並べてがむばって定義していくしかないの
ですがねむいさんはそんなことやる気力がないので…
Jtagkey2とSPI-ROM書き込みプログラム"Flashom"にてFONTX2データを
書き込むことにしました。

使用するフォントは12x12pxの小夏フォントです。
AnkはKONATHN12.FNTを、S-JisのKanjiはKONATZN12.FNTを使用します。
上記ファイルについて、私のL5プロジェクトの下記ディレクトリを
参照してください。
./lib/FONTX2/inc/fonts/KONATSU


※小夏フォントについてはTTFファイルで提供されております。
今回はあらかじめFONTX2コンバータでTTF->FONTX2形式に変換
して全角/半角とも使用しております。



まずはSPI-ROMの認識と読み出し。ちゃんと認識されているのを確認。
ついでに読み出したデータは後で使用するのでbin形式で保存します。



JtagKey2(で使用されているFT2232H)のSCKは最大30MHzのスピートなので
配線長はなるべく短くしましょう。5cm以下だとまず問題ないです。
残念ながらげたゲタ基板とNUCLEO基板を合体した状態ではまともに認識
出来なかったのでNUCLEOから5Vだけ拝借してW25Q128JVSIQと一対一で配線
する形になります。なお、CSはプルアップ必須なのですがねむいさんは
念のためCLK除くすべてのI/Oを22kohmでプルアップしてます。
一方CLKはSDMMCと同じく直列終端抵抗を忘れずに!!
ねむいさんはSDMMCと同じ33ohmを直列終端として使用してます。

お次は書き込み用バイナリの作成。
ねむいさんの知る限りではFlashromはアドレスをオフセット指定して
一部の領域だけを書き込むことができず、全領域一気に書き込みしないと
いけないので先に読み出したbinファイルを利用してそこにFONTX2ファイル
を上書きする形で配置しFONTX2を仕込んだROMファイルを作成します。
バイナリ編集プログラム"Stirling"を駆使して0x0000000にANKフォントを
0x00010000にKanjiフォントをそれぞれ上書きモードでコピペ配置します。
オフセットは1バイトでもずれないように注意してください。


Ankは配置されるとこんな感じ


Kanjiはこんなかんじになります。


Stirlingのビットマップ表示だとバイナリイメージはこんな感じです。
まだまだ一杯データ詰め込めますね。


そして書き込み。JtagKey2とFlashromを使用した際は通常のSPIで
書き込むことになりますがW25Q128JVSIQはセクタ書き込み対応なので
16MByteもある容量でも2分もかからず終わっちゃいます。


ねむいさんの公開しているプログラムでは一応作成済みのQSPI-ROM
イメージ(128QVFONT.bin)を添付して簡単な使用手順も書いてますので
酔狂な方はご参考に…。

まぁ私のぶろぐ普段から見られてる方は楽勝だと思います。





そんなわけでSTM32H5からやる予定だったOCTO-SPIでしたがL5でもうまく
使いこなすことができました!
L5でやりたいことは一通り全部やり切ったのでいい感じでSTM32H5へと
繋げて行きたいとおもいます!!

Go to top of page