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年08月01日

STM32マイコン UART受信時必須処理 その2

以前、STM32マイコンでUARTの受信をする際に必須となるエラー処理について紹介しました。その際はフレーミングエラーが生じており、HAL_UART_Abort関数でクリアする方法を紹介しました。

今回は長いデータを連続して送信する際にオーバーランエラーが発生した際の対処方について紹介します。HAL_UART_Abort関数だけオーバーランエラーが発生した際にクリアされず、MPUがハングする場合がありました。環境としてはSTM32H743Z、STM32H7 Cube MCU Packeage 1.7です。

HAL_UART_Abort関数内では下記のようにエラー処理されており、オーバーランエラー、ノイズエラー、パリティーエラー、フレーミングエラーのクリア処理がされています。一見するとオーバーランエラーが解消されそうに見えますが、MPUがハングする現象がありました。

__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_OREF | UART_CLEAR_NEF | UART_CLEAR_PEF | UART_CLEAR_FEF);


HAL_UART_Abort関数内のフラグだけでは十分でないようです。試行錯誤した結果、下記のように受信フラグやオーバーランフラグをクリアすると正常動作に復帰することが確認できました。

if ( __HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE) )
{
    __HAL_UART_CLEAR_FLAG(&huart1,
                                              UART_CLEAR_NEF |
                                              UART_CLEAR_OREF |
                                              UART_FLAG_RXNE |
                                              UART_FLAG_ORE);
}

UART_FLAG_OREエラーが発生する場合は受信フラグやオーバーランフラグ自体をクリアする必要があるようです。エラーでUART受信できなくなるだけでなく、MPUがハングするような現象が発生したため、原因特定に時間を要してしまいました。最初はmalloc等のメモリ関連が原因かと思いましたが、UART単体でも異常が発生したため、UART起因と判明しました。UART回りはエラーケースが様々でなかなか厄介です。
posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする

2020年07月25日

CO2センサ

混み具合や換気状態を監視する用途としても昨今、CO2センサが注目されています。

一般的なCO2の検出方式としてはNDIR(Non Dispersive Infrared)方式とMOX(Metal Oxide)方式の2種類があります。精度面ではNDIRの方がMOXよりも優れています。MOXは空気中の有機物を検出することで間接的にCO2を推定するため、直接CO2濃度を光学的に検出するNDIRに比べて精度では劣ります。一方でNDIRは光学機構を備えるため、コストがMOXに比べると高く、大きさも比較的大きくなります。一方、MOXは半導体素子として構成されるため、非常に小さく、コストも安い特徴があります。

今回はMOXのセンサを紹介します。MOXセンサとして有名なセンサは下記の通りです。

・AMS製 CCS811 (生産終了)

AMS製は最近、生産終了のようでメーカのサイトからも情報が消えていました。Arduino等で簡単に使いたい場合はSGP30の方が情報が豊富でおすすめです。ただ、個体差なのか、値のドリフトが大きく少し不満がありました。最近、改良版のファームが発表され、さらに精度が上がったということでIDT製ZMOD4410を試してみました。

残念ながらZMOD4410のレジスタ情報が非公開であり、IDTのサイトからユーザー登録して取得する必要があります。また、CO2換算処理アルゴリズムについても非公開でバイナリファイルとして配布されているため、Arduino IDE等では使用できません。バイナリファイル(libファイルやaファイル)を組み込むことができる開発環境が必須です。今回はSTM32F303KとSW4STM32を使用しました。

試しの基板としてZMOD4410とHS3001を組み合わせた環境センサを作成してみました。温度、湿度、TVOC、CO2等を測定することが可能です。

env-sen.jpg


実際に使用してみたところ、換気直後は400ppm以下(下限400ppmでそれ以下は測定できない)となり、夜部屋を閉め切った寝室では朝方にかけて800ppm以上となり、換気をすると400ppm以下に収まることが確認できました。以前の改良前のZMOD4410 - 1st GenではSGP30同様に値のドリフトが多少気になりましたが、ZMOD4410 - 2st Genでは学習にニューラルネットワークが適用された変換アルゴリズムでドリフトが低減したように感じました。


zmod_test.jpg


ZMOD4410はバイナリファイル(libファイルやaファイル)を組み込むことができる開発環境が必要で、データシートも公開されていません。さらに変換アルゴリズムライブラリのコードサイズやメモリ使用量が大きく、敷居が高いセンサです。今後はI2CでArduino等からも簡単に扱うことができるZMOD4410専用の変換基板も開発してみたいと思います。また、ZMOD4410、SGP30、NDIRとの同時計測で比較してみたいと思います。
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年07月11日

iCE40HX8K RISC-V導入

今更な感じもしますが、命令セットアーキテクチャ (ISA)がオープンとなっている RISC-Vを試食してみました。ターゲットはiCE40-HX8Kを用いて、こちらのサイトの手順を参考に試食してみました。

手順についてはサイトに詳細に書かれているため省略しますが、hx8kdemoを実装した際のビルド結果を下記に記します。

=== hx8kdemo ===
Number of wires: 4536
Number of wire bits: 9050
Number of public wires: 4536
Number of public wire bits: 9050
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 7053
SB_CARRY 961
SB_DFF 175
SB_DFFE 651
SB_DFFESR 538
SB_DFFESS 58
SB_DFFNSR 4
SB_DFFSR 216
SB_DFFSS 6
SB_IO 4
SB_LUT4 4434
SB_RAM40_4K 6

サンプルコードでは約7000セル、LUTは約4500個の使用率でした。


RISCV.jpg

iCE40-HX8KのUart機能を使用してTeratermから動作確認ができました。picorv32/picosoc内のfirmware.cがデモのソースコードとなっています。

ファームのみのビルドする場合は下記のコマンドでビルドできます。
make hx8kdemo_fw.elf

ファームのみの書き込みする場合は下記のコマンドで書き込みできます。
sudo make hx8kprog_fw

iCE40-HX8Kで試食する前にMACHXO3でRISC-Vの実装を検討しましたが、そのままではRAM容量やクロックが確保できず諦めていました。海外等では工夫してMACHXO3でRISC-Vを実装した例がありましたが、コードが公開されていませんでした。最近、MACHXO3の新シリーズのMACHXO3DでLatticeが公式にRISC-VのIP Coreを公開されました。3.3Vで駆動可能なMACHXO3はiCE40-HX8Kよりも魅力が大きいため、今後、MACHXO3Dの評価ボードで試食してみたいと思います。

posted by Crescent at 00:00| Comment(0) | 電子工作 | このブログの読者になる | 更新情報をチェックする