2020年09月12日

STM32H7とNAND Flashメモリ

以前に紹介したNAND Flashメモリ を搭載したNucleo144 Memory Shieldを用いて、STM32H743と接続した際にH7シリーズ固有の問題に直面したたため、対処方法を少し紹介します。

STM32H7シリーズは非常に高速な上にIキャッシュとDキャッシュを搭載しているため、さらに効率的に処理をすることが可能です。一方でDキャッシュはDMA転送時等にデータ化け(キャッシュの一貫性が崩れる)が生じるため、なかなか厄介です。データ化けが生じる可能性を意識して実装する必要があります。データ化け、キャッシュの一貫性が崩れるとは、例えば、DMA転送の際にメモリからDMAコントローラに書き込んだはずが、キャッシュに溜まっているだけで、DMAコントローラに書き込まれていない、その逆も然りでDMAコントローラのデータを読み出そうとメモリを見てもキャッシュに溜まっているだけでメモリは更新されてないといったことが生じます。

DMA転送時のデータ化けの対処方法についてはこちらのサイトに紹介されています。今回はDMA転送だけでなく、FMCメモリコントローラでも同様のデータ化けが発生することが分かりました。考えてみれば、メモリコマンド指令も同じデータメモリのため、当たり前といえば当たり前ですが、FMCコントローラの場合、データが化けるという現象ではなく、NAND Flashメモリの初期化や書き込みでエラーが発生するという現象でした。データ化けによってメモリコマンドの書き込み、読み込みのデータが化けてエラーとなったようです。HAL_BUSY等のエラーレスポンスからデータ化けと気づくまでに時間を要してしまいました。

NAND Flashメモリの場合、初期化のMPU_Config内に下記のようにEthernetDMAの領域に加えて、NAND Flashメモリで使用する0x80000000以降の領域に対してライトスルーモードに設定します。EthernetDMAの領域の設定についての詳細はこちらに説明があります。

設定の注意点としては領域はMPU_REGION_NUMBER*で順番に設定することです。*が同じだと設定が上書きされてしまいます。

/* Configure the MPU attributes as Device not cacheable for ETH DMA descriptors */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30040000;
MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* Configure the MPU attributes as Normal Non Cacheable for LwIP RAM heap which contains the Tx buffers */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30044000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);

/* Configure the MPU attributes  for NAND Flash */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x80000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_256MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER2;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);


今回はNAND Flashメモリについて紹介しましたが、NOR Flash、QSPI等でも同様だと思います。0x80000000以降の領域に対してライトスルーモードに設定することでエラーが発生せずに読み書きできるようになりました。FMCコントローラの場合、データが化けるという現象ではなく、NAND Flashメモリの初期化や書き込みでエラーが発生しました。当初、タイミング信号のパラメータ設定ミスや各サイズの設定ミスを疑いましたが、FMC単体では動作して、様々な他の機能を入れると動作しないため、機能を削って確認すると最終的にDキャッシュと判明しました。Dキャッシュを完全に無効にすると正常に動作しますが、パフォーマンスを考えると必要な領域のみを個別に設定した方が良さそうです。
FMCの設定についてはこちらのPDFに分かりやすく説明されています。
posted by Crescent at 00:00| Comment(0) | 組込ソフト | このブログの読者になる | 更新情報をチェックする

2020年08月08日

I2Cスレーブデバイス実装

STM32マイコンのHALライブラリを用いてI2Cスレーブデバイスを実装する場合について紹介します。最も簡単なI2Cスレーブデバイスの実装としては同期関数であるHAL_I2C_Slave_Transmit、HAL_I2C_Slave_Receiveを用いて通信する方法があります。ただ、この方法では一般的なI2Cスレーブデバイスのような実装ができません。

一般的なI2Cスレーブデバイスの場合、I2Cマスタデバイスから下記のような順でデータを書き出したり、読み出したりします。

@スレーブアドレス
Aスレーブレジスタ書き込み
Bデータ書き込みor書き込み

同期関数を使用した場合はタイムアウトするまでスレーブデバイスとしてI2C受信処理以外ができなくなってしまうため、非同期関数で受信時以外はスレーブデバイスとしてのセンサ情報管理や取得、ADC変換といった別の処理をさせたほうが現実的だと思います。

今回は非同期で上記のような一般的なI2CスレーブデバイスをSTM32マイコンで実装する場合について紹介します。



@CubeMX設定
I2Cデバイス設定で「Parameter Settings」の「Primary slave address」にI2Cスレーブデバイスを7bitアドレスで設定(例では0x32)します。CubeMXで生成後に自動的にコード上ではRead、Writeビットを含めた8bitアドレスしてビットシフトされます。

非同期でI2Cスレーブ受信処理を行うため、NVIC Settingsで「I2C* event interrupt」、必要に応じて「I2C* error interrupt」にチェックを入れて割込みを有効化します。


Aコード実装
グローバル変数として下記を定義します。
volatile uint8_t TransferDirection, TransferRequested;
volatile uint8_t SensorData;

#define I2C_SLAVE_ADDR (0x32<<1)
#define SENSOR_REGISTER 0x1
#define WHO_AM_I_REGISTER 0x0
#define WHO_AM_I_VALUE 0xBC
#define TRANSFER_DIR_WRITE 0x1
#define TRANSFER_DIR_READ 0x0

グローバル関数として割込み関数を定義します。

void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {
   if(hi2c->Instance == I2C1) {
     if(AddrMatchCode== I2C_SLAVE_ADDR)
     {
       transferRequested = 1;
       transferDirection = TransferDirection;
     }
   }
}

void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if(hi2c->Instance==I2C1){
      HAL_I2C_EnableListen_IT(&hi2c1);
    }
}


int main(void)
{
 〜〜〜〜
 //自動生成初期化コード
 〜〜〜〜
/* USER CODE BEGIN 2 */
 HAL_I2C_EnableListen_IT(&hi2c1);//スレーブアドレス呼び出しの割込み有効化
uint8_t i2cBuf[2];
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
 while (1)
 {

    while(!TransferRequested) {//I2Cスレーブ処理がない場合
       SensorData=XXXX;//センサデータ更新
    }
    TransferRequested = 0;
    if(TransferDirection == TRANSFER_DIR_WRITE) {
      HAL_I2C_Slave_Sequential_Receive_IT(&hi2c1, i2cBuf, 1, I2C_FIRST_FRAME);//レジスタ情報受信
      while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_LISTEN);

      switch(i2cBuf[0]) {//レジスタ情報に応じて返す値を変える
        case WHO_AM_I_REGISTER:
            i2cBuf[0] = WHO_AM_I_VALUE;
            break;
        case SENSOR_REGISTER :
            i2cBuf[0] = SensorData;
            break;
        default:
            i2cBuf[0] = 0xFF;
       }
       HAL_I2C_Slave_Sequential_Transmit_IT(&hi2c1, i2cBuf, 1, I2C_LAST_FRAME);
       while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
   }

/* USER CODE END WHILE */
 }

ポイントとしてはHAL_I2C_EnableListen_ITでスレーブアドレスの受信割込みを有効化し、HAL_I2C_Slave_Sequential_Receive_ITとHAL_I2C_Slave_Sequential_Transmit_ITを用いて受信、送信処理をする点です。また、受信、送信処理ではack、nackを返す処理をするためにI2C_FIRST_FRAME、I2C_NEXT_FRAME、I2C_LAST_FRAME等を使い分ける必要があります。

今回の例ではレジスタ値によって返す値の数を1つに固定しましたが、I2Cスレーブデバイスによっては連続するレジスタの値を一度に取得することが可能なものがあります。そのような場合はレジスタの値を配列に予め入れて置き、要求のあったレジスタによってオフセットさせて値を返すようにすることで実現可能です。

STM32マイコンで自作I2Cデバイスを製作してみたいと思います。
posted by Crescent at 00:00| Comment(0) | 組込ソフト | このブログの読者になる | 更新情報をチェックする

2020年07月18日

lwipを用いたFTP Client

イーサネットIFを搭載したSTM32マイコンではCubeMXを用いてイーサネット通信が容易に行うことができます。NucleoボードやDiscoveryボードでは基板上にETHPHYのコントローラICを搭載しているものも多くあり、そのままイーサネット通信の実験を行うことが可能です。

今回はSTM32マイコンを用いてFTPサーバーにデータを格納する方法について紹介します。STM32マイコンがFTPクライアント、サーバーがIIS FTPサーバー@Windows10の環境で実験しました。また、STM32マイコンのイーサネット通信ライブラリとしてlwipを使用し、FTPクライアントはlwftpライブラリを使用しました。lwftpのサンプルコードではそのままでは動かない点や分かりづらい点があったのでそこを踏まえて紹介します。なお、lwftpライブラリはパッシブ通信のFTPクライアントのみサポートしています。

グローバル変数としてセッションを宣言します。
static lwftp_session_t s;


main関数内のメイン処理は下記の通りです。
必要に応じてFTPサーバーのIP、ログインID、ログインパスワードを変更してください。

 // Initialize session data
 memset(&s, 0, sizeof(s));
 IP4_ADDR(&s.server_ip, 192,168,6,3);
 s.server_port = 21;
 s.done_fn = ftp_connect_callback;
 s.user = "ftpuser";
 s.pass = "test";
 s.handle = &s;
 // Start the connection state machine
 error = lwftp_connect(&s);
 if ( error != LWFTP_RESULT_INPROGRESS ) {
  printf("lwftp_connect failed (%d)", error);
 }
 else{
  printf("FTP END \n\r");
 }
 while(1);



main関数の外のグローバル関数として下記のコールバック関数を宣言します。

//FTP通信完了に呼び出される
static void ftp_retr_callback(void *arg, int result)
{
 lwftp_session_t *s = (lwftp_session_t*)arg;
 if ( result != LWFTP_RESULT_OK ){
  LOG_ERROR("retr failed (%d)", result);
  return lwftp_close(s);
 }
 lwftp_close(s);
}

//未使用、FTPサーバからデータ受信で使用
static uint data_sink(void *arg, const char* ptr, uint len)
{
 static const uint mylen = 10;
 static char * const myconfig = (char*)0x20000000;
 static uint offset = 0;
 if (ptr) {
 len = min( len, mylen-offset );
 memcpy( myconfig+offset, ptr, len );
 offset += len;
 }
 return len;
}


static void ftp_stor_callback(void *arg, int result)
{
 lwftp_session_t *s = (lwftp_session_t*)arg;
 err_t error;
 if(s->control_state==LWFTP_DATAEND && result == LWFTP_RESULT_OK ){
  s->control_state=LWFTP_DATAEND;
  return lwftp_close(s);
 }
 if ( result != LWFTP_RESULT_OK && result != LWFTP_RESULT_INPROGRESS && result != LWFTP_RESULT_LOGGED ) {
  LOG_ERROR("stor failed (%d)", result);
  return lwftp_close(s);
 }
 if(s->control_state==LWFTP_CLOSED && result == LWFTP_RESULT_OK)return;
 if(result == LWFTP_RESULT_INPROGRESS)return;
 s->data_sink = data_sink;
 s->done_fn = ftp_retr_callback;
 s->remote_path = "new";
 error = lwftp_retrieve(s);
 if ( result != LWFTP_RESULT_OK && error != LWFTP_RESULT_INPROGRESS && result != LWFTP_RESULT_LOGGED ) {
 LOG_ERROR("lwftp_retrieve failed (%d)", error);
 }
}

//データ元の関数、今回はTEST_DATA:abcdefgの文字列を格納する
char test_data[]={"TEST_DATA:abcdefg"};
static uint data_source(void *arg, const char** pptr, uint maxlen)
{
 static const uint mylen = sizeof(test_data);;
 static const char * const mydata = (char*)&test_data;
 static uint offset = 0;
 uint len = 0;
 if (pptr) {
  len = mylen - offset;
  if ( len > maxlen ) len = maxlen;
  *pptr = mydata + offset;
 }
else {
 offset += maxlen;
 if ( offset > mylen ) offset = mylen;
 }
 return len;
}

//FTP通信接続成功で呼び出される
//logfile.txtというファイル名でFTPサーバーに格納する
static void ftp_connect_callback(void *arg, int result)
{
 lwftp_session_t *s = (lwftp_session_t*)arg;
 err_t error;
 if( result == LWFTP_RESULT_INPROGRESS ){
  return;
 }
 if ( result != LWFTP_RESULT_LOGGED ) {
  LOG_ERROR("login failed (%d)", result);
  return lwftp_close(s);
 }
 s->data_source = data_source;
 s->done_fn = ftp_stor_callback;
 s->remote_path = "logfile.txt";
 error = lwftp_store(s);
 if ( error != LWFTP_RESULT_INPROGRESS ) {
  LOG_ERROR("lwftp_store failed (%d)", error);
 }
}


lwipはコールバック関数で接続や受信などのイベント発生毎に関数が呼び出される構造となっており、lwftpについても同様にイベント毎に関数が実行されます。サンプルコードでは各コールバック関数内のcontrol_state状態に応じた分岐処理がうまくいかず、FTPサーバーに格納できませんでした。Wiresharkで動作を確認しながら、control_stateの分岐条件を追加することでFTPサーバーにデータを格納できるようになりました。lwftpを用いてちょっとしたデータロガーとして応用できそうです。
posted by Crescent at 00:00| Comment(0) | 組込ソフト | このブログの読者になる | 更新情報をチェックする

2020年04月18日

メモリ空き領域確認方法

今回はSW4STM32といったarm-gcc環境でビルド時にメモリ領域を確認する方法を紹介します。


簡単なプログラムであれば、FlashやRamの領域を気にする必要はありませんが、大量にメモリを使用する場合はどれくらい既に占有しているのか気になると思います。リンカの設定を少し追加することで詳細情報をビルド時のコンソールに表示することが可能です。

デフォルトではビルド後のコンソール画面に下記のように表示されます。

〜〜〜
directory post-build
Generating hex and Printing size information:
arm-none-eabi-objcopy -O ihex "***.elf" "***.hex"
arm-none-eabi-size "***.elf"
text        data     bss      dec       hex      filename
22604     484     1828    24916   6154    ***.elf
**:**:** Build Finished (took **s.**ms)
〜〜〜

メモリ使用量はtext、data、bssという形で分かれて情報が表示されます。

.text プログラムコード、通常はFlash
.data 初期値を持つ静的変数、通常はRAM
.bss 初期値を持たない静的変数、通常はRAM

ただ、正直、text、data、bssという表示では分かりにくいと思います。リンカの設定を少し追加すると詳細情報をビルド時に表示することが可能です。

プロジェクトの「プロパティ」から「C/C++ Build」、「Settings」、「Tool Settings」、「MUC GCC Linker」のCommand欄に設定を追加します。

Command欄の初期値は
gcc
となっていますが、
-Wl,--print-memory-usage
を追加して
gcc -Wl,--print-memory-usage
にします。



Linker-Setting.jpg
実際に設定した際の設定画面はこんな感じです



この設定状態でビルドすると...
〜〜〜
Memory region  Used Size   Region Size   %age Used
         RAM:              2080 B         320 KB              0.63%
      FLASH:            15284 B            1 MB      1.46%
     SDRAM:          341600 B            8 MB              4.07%

Finished building target: ***.elf

C:*** print-directory post-build
Generating hex and Printing size information:
arm-none-eabi-objcopy -O ihex "***.elf" "***.hex"
arm-none-eabi-size "***.elf"
text        data     bss      dec       hex      filename
15156     120     343568 358844 579bc  ***.elf
**:**:** Build Finished (took **s.**ms)
〜〜〜

という感じでメモリの使用率とサイズが表示され、どれくらいメモリを占有しているのか一目で分かります。なお、通常はSDRAMがなければSDRAMの項目は表示されません。SW4STM32以外でもSTM32CubeIDEといったarm-gcc系であれば同様に使えると思います。大量にメモリを使用する場合にはぜひ、このような設定を有効化してみてください。


posted by Crescent at 00:00| Comment(0) | 組込ソフト | このブログの読者になる | 更新情報をチェックする

2020年03月28日

STM32H7 lwipの設定

STM32H743ZIでCubeMXで生成したコードでRTOSなしでlwipを用いた通信ができないという不具合がありました。使用しているHALライブラリバージョンはSTM32Cube FW_H7 V1.7.0、CubeMX5.6です。統合開発環境はSW4STM32、v2.9です。

HALライブラリ内のサンプルプログラムは正常に動くものの、CubeMXで生成したファイルではEthenetが正常に動かず、pingが通らない状況でした。

コード等を完全にコピーしても不具合が治らないため、さらに原因をいろいろ調査すると...リンカスクリプトが原因と分かりました。プロジェクトファイル直下にCubeMXで生成されるSTM32H743ZITx_FLASH.ldです。

中身を比較テキストエディタ、WinMergeで確認すると不具合の原因が分かりました。


左が正常に動くリンカスクリプト、右がCubeMXで生成した不具合のリンカスクリプトです。
正常に動くサンプルプログラムはSTM32Cube_FW_H7_V1.7.0\Projects\NUCLEO-H743ZI\Applications\LwIP\LwIP_HTTP_Server_Netconn_RTOS\SW4STM32\STM32H743ZI_Nucleoを参照しました。


winmerge.jpg

lwipで使用するEthernet送受信領域がリンカスクリプト内で定義されていませんでした。そのため、未定義のままでEthenet正常に動かなかったようです。他にもリンカスクリプト内でヒープサイズ等の異なる点がありましたが、Ethenetの動作上は問題ありませんでした。

.lwip_sec (NOLOAD) : {
. = ABSOLUTE(0x30040000);
*(.RxDecripSection)

. = ABSOLUTE(0x30040060);
*(.TxDecripSection)

. = ABSOLUTE(0x30040200);
*(.RxArraySection)
} >RAM_D2 AT> FLASH

上記の定義をリンカスクリプトに追記すると正常に動作することが確認できました。CubeMX上でETHとlwipの設定をしているにも関わらず、リンカスクリプトに反映されないのはCubeMXの不具合だと思われます。今後のバージョンアップで修正に期待したいと思います。
posted by Crescent at 00:00| Comment(0) | 組込ソフト | このブログの読者になる | 更新情報をチェックする