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へと
繋げて行きたいとおもいます!!

STM32L5を使ってみる2 -FatFsその他の実装-

●いつものやつを実装しよう
前回は144Pin版NUCLEO基板の下駄基板のビジュアルだけ紹介しましたが、
今回はそれを動かすためのファームウエアの要所を紹介していこうと思います。
ねむいさんの"いつもの"ことChaN師のFatFs実装を中心としております。

以下、STM32L5-NUCLEOのFatFs実装例のプロジェクトの要点を掻い摘んで
解説していきたいと思います。

●GCCビルドのためのCortex-M33向けオプション
先ずはmakefileを見ていただいたらお分かりと思いますがCortex-M33は
基本的にCortex-M4Fの進化版と思って頂けたらよいと思います。
GCCコンパイラに与えるCortex-M33コア向けオプションは以下になります。

-mcpu=cortex-m33 -mtune=cortex-m33 -mfix-cmse-cve-2021-35465

-mfix-cmse-cve-2021-35465とかいうのはArmv8-Mコアの脆弱性対策です。
2023年現在の最新のGCCコンパイラはデフォルトでONになっておりいちいち
入れる必要はないかもしれませんが念のため明示してます。




●基本の"き"、タイマーとUARTの実装
STM32に代表されるCortex-M系のコアはほぼすべてにSystickタイマーが
標準装備されております。こちらを1mSecウエイト用のタイマーとして、一方
マイコン周辺機器のタイマー機能を利用してuSecオーダーのタイマーを実装します。
今回はuSec用にはTIM5を利用しました。

以下systick.cより抜粋
	/* Making MicroSecond-Order Timer uses general purpose timer! */
/* Enable timer clock */
USEC_TIMx_CLKEN();

/* calculate TIMx(2 or 5) Prescaler clock(APB1) */
if(RCC->CFGR & RCC_CFGR_PPRE1){
if((RCC->CFGR & RCC_CFGR_PPRE1) == RCC_HCLK_DIV2){
cal_usec_divide = SystemCoreClock/2; /* (HCLK(=SYSCLK)*2 */
}
else if((RCC->CFGR & RCC_CFGR_PPRE1) == RCC_HCLK_DIV4){
cal_usec_divide = SystemCoreClock/4; /* (HCLK(=SYSCLK)*4)*2 */
}
else if((RCC->CFGR & RCC_CFGR_PPRE1) == RCC_HCLK_DIV8){
cal_usec_divide = SystemCoreClock/8; /* (HCLK(=SYSCLK)*8)*2 */
}
else if((RCC->CFGR & RCC_CFGR_PPRE1) == RCC_HCLK_DIV16){
cal_usec_divide = SystemCoreClock/16; /* (HCLK(=SYSCLK)*16)*2 */
}
}

/* usec wait timer settings */
TimHandle.Instance = USEC_TIMx;
TimHandle.Init.Period = UINT32_MAX;
TimHandle.Init.Prescaler = ((cal_usec_divide)/USEC_INTERVAL) - 1;
TimHandle.Init.ClockDivision = 0;
TimHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
TimHandle.Init.RepetitionCounter = 0;
TimHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&TimHandle) != HAL_OK)
{
/* Capture error */
while (1);
}

USEC_TIMx->CR1 &= ~(TIM_CR1_UIFREMAP); /* Disable UIF Remap(Must Need!) */
HAL_TIM_Base_Start(&TimHandle);

最近のHALライブラリは"AutoReloadPreload"構造体が追加されているので
移植の際はご注意ください。また,UIFRemapビットにもご注意ください。


UARTについてはNUCLEO-L552ZE-Q基板上ではSTLinkのVCPとはLPUART
なるもので接続されております。一般のUARTとは多少違いがありますが
やることは同じです。LPUART1、GPIOG7,8をTX,RXとします。
一応LPUART以外のポートも使用できるようにしておりますので詳細は
uart.cupport.cを参照願います(丸投げ)
っとその前にLPUART1を使う際はLPUART1へのクロック供給のほかに
"HAL_PWREx_EnableVddIO2()"の実行も忘れないようにしてください。

またリングバッファによるノンブロッキング送受信なのはいつも通り
なのでスムーズにデータのやり取りができると思います。


●SDMMCドライバの実装・FatFsとの結合
STM32L5にはSDMMCがありますのでこれを実装しますが数年前STM32H7に
実装したSDMMCのドライバ
をスライド移植するだけのお手軽です☆
ぶっちゃけ今回の移植作業で一番楽でした。


一応使用上の注意なのですがSTM32H7と同じくSTM32L5のSDMMCはDDR
モードが使用可能ですがSDMMCに供給するクロックがバイパスされて
いないと使用不可となりますのでご注意ください。

ねむいさんの作例ではデフォルトではHSI48をSDMMCにバイパスなしで
供給しているためDDRで使用することができませんorz


もしDDR対応のeMMCをお持ちの方はコアクロックを110MHzから100MHzで
動かすとHSI48を使わずコアクロックをSDMMCのクロックにするよう
切り替えますので試してみてください。

STM32L5の最大動作クロックは110MHzなんですけどこういう制約が色々
あって100MHzで動かすほうがよっぽど有利なんですよね〜
100MHzとか昨今のマイコンでは遅い部類のクロックですがまぁ初代の
STM32F1とかのMAX72MHz動作と比べたら(消費電力も合間見ると)雲泥の
差ですがすっかり贅沢になってしまいましたね〜

STM32H7の実装でも実はひっそり最高速の480MHz対応してますが
こちらもいずれ紹介しようと思います。


●SPIとシリアル接続TFT-LCD

STM32L5のSPIモジュールも言ったって普通のSPIなので実装は容易
でした…が、DMAのほうはちょっと一癖ありました。
ねむいさんが知ってる従来のDMAではなくってDMAのチャネルがマルチ
プレクスされていて柔軟な設定ができるようになっており
、逆に扱いに
苦労しましたがExampleを参考に何とかSPIでDMAを吐けるようになって
おります。以下にDMAMUXの設定を抜粋しておきます。
全体は./lib/display/mcu_depend/display_if_basis.cを参照のこと。
#ifdef USE_DISPLAY_DMA_TRANSFER
/* DMA controller clock enable */
__HAL_RCC_DMAMUX1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();

/* Configure DMA request LcdDmaHandle */
LcdDmaHandle.Instance = SPILCD_DMA_CHANNEL;
LcdDmaHandle.Init.Request = SPILCD_DMA_REQEST;
LcdDmaHandle.Init.Direction = DMA_MEMORY_TO_PERIPH;
LcdDmaHandle.Init.PeriphInc = DMA_PINC_DISABLE;
LcdDmaHandle.Init.MemInc = DMA_MINC_ENABLE;
LcdDmaHandle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
LcdDmaHandle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
LcdDmaHandle.Init.Mode = DMA_NORMAL;
LcdDmaHandle.Init.Priority = DMA_PRIORITY_MEDIUM;
if (HAL_DMA_Init(&LcdDmaHandle) != HAL_OK)
{
for(;;){
__NOP();
}
}
if (HAL_DMA_ConfigChannelAttributes(&LcdDmaHandle, DMA_CHANNEL_NPRIV) != HAL_OK)
{
for(;;){
__NOP();
}
}
__HAL_LINKDMA(&SpiHandle,hdmatx,LcdDmaHandle);

/* DMA interrupt init */
/* SPILCD_DMA_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SPILCD_DMA_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(SPILCD_DMA_IRQn);
#endif


また下駄基板ではST7789V2を使用したTFT-LCDモジュールを採用して
おりますがこちらのデータ線はSDIとSDOがマルチプレクスされたSDA
となっており、ST7789V2のデバイスIDを取得したいならばSTM32L5側で
SPIのMOSIとMISOを直結し読み出しの際にMISOの信号を正しく受け取る
MOSIをGPIOの入力にわざわざ切り替えてやる必要があります。
以下にそのコードを示します。
void Display_ChangeSDA_If(uint8_t sda_mode)
{
GPIO_InitTypeDef GPIO_InitStructure;

if(sda_mode == TFT_SDA_READ){
/* Enable CTRL Line GPIO Settings */
DISPLAY_GPIOCLK_EN(DISPLAY_CLK_SDI);
GPIO_InitStructure.Pin = CTRL_SDI;
GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStructure.Alternate = 0;
HAL_GPIO_Init(DISPLAY_PORT_SDI, &GPIO_InitStructure);
}

else {
/* Enable CTRL Line GPIO Settings */
#if defined(USE_SOFTWARE_SPI)
DISPLAY_GPIOCLK_EN(DISPLAY_CLK_SDI);
GPIO_InitStructure.Pin = CTRL_SDI;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStructure.Alternate = 0;
HAL_GPIO_Init(DISPLAY_PORT_SDI, &GPIO_InitStructure);

#else
/* Connect SPI pins to Alternate Function */
/* Restore SPI MOSI pin configuration */
DISPLAY_GPIOCLK_EN(DISPLAY_CLK_SDI);
DISPLAY_PERIF_CLK(ENABLE);
GPIO_InitStructure.Pin = CTRL_SDI;
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStructure.Alternate = SRC_SDI;
HAL_GPIO_Init(DISPLAY_PORT_SDI, &GPIO_InitStructure);
#endif
}
}





ちなみにST7789V2側のドライバはデフォルトでは上記の切り替えの
必要なくIDを決め打ちで返すようにしてますのでもの好きな方だけ
SDAマルチプレクス有効にして試してみてください。
SDAマルチプレクスの際はSTM32側のMOSIとMISOをショートしてください。



ST7789V2からIDを読み出す設定でビルドしてデバッガで追っかけてみると
こんな感じにSDAからST7789V2のID(RDID2(0xDB)で0x85が返る)が
読まれてくるのがわかります。

SPI接続で動くモジュールはこんな感じでピン数節約のためにSDIとSDOが
マルチプレクスされてることもあるので覚えておいて損はないでしょう。



というわけでいつものChaN師のファイラー起動からの…

海で働いてるいないさんのpng形式のイラスツをlibpngでデコード!
(LCDのサイズ小さいので画像の一部しか表示できませんが)

libpngのほかにbmp形式はもちろんlibjpegやgiflibでjpg、gif形式
ファイルも表示できますがSTM32L5シリーズはフラッシュメモリ容量が
512kByteしかないのでフォントファイルを乗せつつ画像デコード用の
ライブラリ全部乗せは容量上非常に苦しいです。

そんなときのために容量を食うフォントファイルは外付けのSPI-ROM
にデータを配置してアクセスしたいところですが…
こういう用途に便利なOCTO-SPIという機能がありますが解説が長く
なりそうなので次回に続きます…!

Go to top of page