2017年8月19日土曜日

BME280をPIC32MX250F128B+Harmonyで使う

前回に引き続き、I2CセンサをPIC32で扱うお話です。

BME280はBOSCH製の温湿度・気圧センサです。
個人的にはBOSCHと言えばドイツの自動車部品メーカーというイメージですが、こういった半導体部品も作ってるんですね。驚きです。

さてさて、こいつもI2C/SPI両対応のチップで、分解能が温度0.01℃、湿度0.008%、気圧0.18Paとかなり細かくデータを取ることができます。データシートもドイツメーカーだからか英語が非常にシンプルで読みやすく、内容もコンパクトにまとめられています。

このセンサの使用方法としては
  1. 設定の書き込み
  2. Trimming Parameterの読み込み
  3. (生の)測定データの読み込み
  4. 測定データの演算
の流れとなっています。
Trimming ParameterはCalibration Dataとも書かれており、生の測定値を℃、%、hPaに変換するにあたって使われる係数です。
センサによって個体差があるので、メーカー出荷時にその個体差に合わせて係数を設定しておき、ユーザーは生の測定データをTrimming Parameterを使って演算することで正確な温湿度や気圧を得ることができます。

順に追って見ていきましょう。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool I2C_InitAndOpen(I2C_HANDLE *pobj, SYS_MODULE_INDEX index)
{
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    
    if(pobj->hDriver == (DRV_HANDLE)NULL)
        return false;
    
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return true;
}

DRV_I2C_BUFFER_EVENT I2C_GetTransferStatus(I2C_HANDLE *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool I2C_IsCommunicating(I2C_HANDLE *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

uint32_t I2C_GetTransferredBytes(I2C_HANDLE *pobj)
{
    return DRV_I2C_BytesTransferred(pobj->hDriver, pobj->hBufferDriver);
}

まずはI2C周りのコードです。I2CバスをBME280とMPU-9250で共有しているので、共通するI2C関係のプログラムを分離しています。

#define BME280_SENDDATA_COUNT_MAX  4

typedef struct {
    I2C_HANDLE *pHandle;
    uint8_t WriteBuffer[BME280_SENDDATA_COUNT_MAX * 2];
    uint8_t SlaveAddress;
    
    uint16_t dig_T1;
    int16_t dig_T2;
    int16_t dig_T3;
    uint16_t dig_P1;
    int16_t dig_P2;
    int16_t dig_P3;
    int16_t dig_P4;
    int16_t dig_P5;
    int16_t dig_P6;
    int16_t dig_P7;
    int16_t dig_P8;
    int16_t dig_P9;
    uint8_t dig_H1;    
    int16_t dig_H2;    
    uint8_t dig_H3;    
    int16_t dig_H4;    
    int16_t dig_H5;    
    int8_t dig_H6;    
} BME280;

_Bool BME280_Init(BME280 *pobj, I2C_HANDLE *pHandle, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    pobj->pHandle = pHandle;

    return true;
}

そのためBME280の初期化関数はアドレスの登録だけになっています。
ちなみにBME280のデータを保持する構造体にはdig_**みたいな変数をたくさん用意してありますがこれらがTrimming Parametersです。配列にしたほうがカッコイイ気もしますが、データシート等に倣ってこう書いておくことにします。

_Bool BME280_StartWritingResistor(BME280 *pobj, uint8_t address, uint8_t data)
{
    BME280_DATA_ADDRESS_PAIR pair;
    
    pair.Address = address;
    pair.Data = data;
    
    return BME280_StartBurstWritingResistor(pobj, &pair, 1);
}

_Bool BME280_StartBurstWritingResistor(BME280 *pobj, BME280_DATA_ADDRESS_PAIR *pairs, int count)
{
    int i;
    
    if(count <= 0 || count > BME280_SENDDATA_COUNT_MAX || I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    for(i = 0; i < count; i++) {
        pobj->WriteBuffer[i * 2 + 0] = pairs[i].Address;
        pobj->WriteBuffer[i * 2 + 1] = pairs[i].Data;
    }
    
    pobj->pHandle->hBufferDriver = DRV_I2C_Transmit(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, count * 2, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

_Bool BME280_StartReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata)
{
    return BME280_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool BME280_StartBurstReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->pHandle->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

送受信関係のコードです。
基本的には、書きこみの場合はレジスタアドレスを送ってからデータ内容を送信、読み込みの場合はレジスタアドレスを送ってから読み込みモードでリスタートして受信という形になります。

ただ、複数レジスタを同時に読み書きするburst write / readは読み込むときと書きこむときで仕様が違います。
読み込みの場合はレジスタが自動的にインクリメントされるので、読み込みたいアドレスの先頭のアドレスを指定してやって読みたいバイト数分のデータを連続で読み込めば良いです。
書き込みの場合は自動的にインクリメントはされません。その代わり、「レジスタアドレス+書きこむ値」の計2バイトをセットで送る形になります。
読み込みは測定値のレジスタアドレスが連番になっていますから自動インクリメントが助かりますが、書きこみの場合は設定になるので連番とは限らず、このような書きこみ方ができるとかえって助かります。

ところで、湿度は16bit、温度と気圧は20bit値なわけで、2バイトないしは3バイトで1つのデータとなっています。この手の複数バイトを1バイトずつ分割して転送する処理をするときは「最初のバイトを送信してから最後のバイトを送信し終わるまでの間に値が更新されたらどうなるのか?」という疑問が付きまといます。
MPU-9250のときも気になってデータシートを探したのですが、特にそのようなことは書かれていませんでした。 ですが、このBME280にはそれに関する記述がありました。
In normal mode, the timing of measurements is not necessarily synchronized to the readout by the user. This means that new measurement results may become available while the user is reading the results from the previous measurement. In this case, shadowing is performed in order to guarantee data consistency. Shadowing will only work if all data registers are read in a single burst read. Therefore, the user must use burst reads if he does not synchronize data readout with the measurement cycle. Using several independent read commands may result in inconsistent data.
If a new measurement is finished and the data registers are still being read, the new measurement results are transferred into shadow data registers. The content of shadow registers is transferred into data registers as soon as the user ends the burst read, even if not all data registers were read.
The end of the burst read is marked by the rising edge of CSB pin in SPI case or by the recognition of a stop condition in I2C case. After the end of the burst read, all user data registers are updated at once. (データシート4.1章「Data register shadowing」より引用)
(拙訳)ノーマルモードでは、測定のタイミングはデータの読み出しと同期されるとは限りません。これはすなわち、ひとつ前の測定値を読み出しているときに新たな測定が完了する可能性があるということを意味しています。そのような場合は、シャドーイングがデータの整合性を保証します。シャドーイングはデータレジスタを1回のバーストリードで読み出しているときのみ機能します。そのため、データの測定サイクルに同期せずに読み出す場合は必ずバーストリードを行う必要があります。個々のレジスタを個別に読みだすとデータの整合性が崩れる可能性があります。
測定が終了したときがレジスタの読み出しをしている最中だった場合、その新しい測定値はシャドーレジスタに転送されます。バーストリードが終了したタイミングでシャドーレジスタの値はデータレジスタに転送されるため、他のレジスタをこれから読み出す予定だったとしてもそれは感知されません。
バーストリードの終了は、SPI接続の場合はCSBピンの立ち上がりエッジ、I2C接続の場合はストップビットで判断されます。バーストリードの終了時に、すべてのデータレジスタは一度に更新されます。
英文を読んでいると何度も同じようなことを言っていて混乱してきますが、要するに、バーストリード中に測定が完了した場合、新しい測定値はシャドーレジスタ(本来読み出すレジスタに書き込みができないときに、いったん測定データを保持しておくためのレジスタ。本来のレジスタの影のような存在であるため、このような名前になっている。)に一旦保存され、バーストリードの終了とともに読み出し用レジスタに転送されます。このため、データの整合性は保証されるということです。
データの整合性とは、マルチバイトデータのそれぞれのバイトが単一の測定から得られたものか、という意味以外にも、温度データと気圧データが同じ時刻に測定されたものである、という意味まで含んでいると思われます。ですので、原則として、このセンサではすべての測定値を同時にバーストリードする必要があります。


さて、今回は設定をこのようにしました。

BME280_RV_CONFIG_t config;
BME280_RV_CTRL_HUM_t ctrl_hum;
BME280_RV_CTRL_MEAS_t ctrl_meas;
BME280_DATA_ADDRESS_PAIR data[3];

ctrl_hum.OSRS_H = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_P = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_T = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.MODE = BME280_VAL_MODE_NORMAL;
config.T_SB = BME280_VAL_TIME_STANDBY_0_5;
config.FILTER = BME280_VAL_FILTER_OFF;
config.SPI3W_EN = 0;

data[0].Address = BME280_RA_CTRL_HUM;
data[0].Data = ctrl_hum.Value;
data[1].Address = BME280_RA_CTRL_MEAS;
data[1].Data = ctrl_meas.Value;
data[2].Address = BME280_RA_CONFIG;
data[2].Data = config.Value;

if(BME280_StartBurstWritingResistor(&appData.bme280, data, 3))
    appData.state = APP_STATE_BME280_CHECK_CONFIG;
else {
    strcpy(szErrorMessage, "ERROR on APP_STATE_BME280_WRITE_CONFIG\r\n");
    appData.state = APP_STATE_ERROR;
}
break;

各設定用レジスタの値はビットフィールド構造体を作ってあげて、マクロでわかりやすく設定できるようにしました。
同じ物理量を複数回測定することで精度を出す手法「オーバーサンプリング」を使うことができますが、その分サンプリング周波数が落ちるので今回は使わないことにしています。
モードはスリープ(SLEEP)、1回のみ測定(FORCE)、連続で測定(NORMAL)の3種類から選べます。今回はもちろんNORMALです。
また、測定をしてから次の測定までのインターバルを設定することができます。今回は最短の0.5msにしましたが、あくまでもそれはインターバルで、測定周期とは異なることに注意が必要です。この辺の時間の見積もり方はデータシートに詳しく載っています。

さて、設定をしたら次はTrimming Parameterを読み込まなくてはいけません。
レジスタはアドレス0x88~0xA1の26バイトと、0xE1~0xF0の16バイトに分かれて格納されていますが、このうち使うのは前者26バイト分と後者7バイト分のようです。
かなり不規則なデータの入れ方をしていますが、格納するときはこのようなプログラムになります。

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_Low(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_T1 = ((uint16_t)pdata[ 1] << 8) | pdata[ 0];
        pobj->dig_T2 = ((uint16_t)pdata[ 3] << 8) | pdata[ 2];
        pobj->dig_T3 = ((uint16_t)pdata[ 5] << 8) | pdata[ 4];
        pobj->dig_P1 = ((uint16_t)pdata[ 7] << 8) | pdata[ 6];
        pobj->dig_P2 = ((uint16_t)pdata[ 9] << 8) | pdata[ 8];
        pobj->dig_P3 = ((uint16_t)pdata[11] << 8) | pdata[10];
        pobj->dig_P4 = ((uint16_t)pdata[13] << 8) | pdata[12];
        pobj->dig_P5 = ((uint16_t)pdata[15] << 8) | pdata[14];
        pobj->dig_P6 = ((uint16_t)pdata[17] << 8) | pdata[16];
        pobj->dig_P7 = ((uint16_t)pdata[19] << 8) | pdata[18];
        pobj->dig_P8 = ((uint16_t)pdata[21] << 8) | pdata[20];
        pobj->dig_P9 = ((uint16_t)pdata[23] << 8) | pdata[22];
        pobj->dig_H1 =            pdata[25];
    }
    return event;
}

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_High(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_H2 = ((uint16_t)pdata[1] << 8) |   pdata[0];
        pobj->dig_H3 =                               pdata[2];
        pobj->dig_H4 = ((uint16_t)pdata[3] << 4) | ( pdata[4]       & 0x0F);
        pobj->dig_H5 = ((uint16_t)pdata[5] << 4) | ((pdata[4] >> 4) & 0x0F);
        pobj->dig_H6 =                               pdata[6];
    }
    return event;
}

前半部分で偶数バイトは下位、奇数バイトは上位としているのかと思いきやdig_H1だけ違ったり、後半部分では4ビットずつ使うところなんかが出てきていささか気持ち悪いですが、とりあえずこんな感じのコードになりました。


さて、今度はこれを使って生の測定値を変換することになりますが、これまた非常にわかりにくい処理になっています。

データシートを見ると、コードが書いてあるだけで、具体的にどのような数式で処理をしているかはっきりしません。変数もval1などと言った意味のない名前で、使いまわしもされており、完全に人に読ませるようなコードではありません。
もっと言えば、データシートでは符号付き整数型のシフト演算がされており、負の値に対するシフト演算はC言語では未定義(処理系依存)の動作となります。そのうえ、データシートに「値の変換はBosch Sensortecから出ているAPIを使うことを推奨します」と書かれている始末です。じゃあなんのためにデータシートにこんなわけわからんコード載せてんねん。
Bosch Sensortec BME280 sensor driver
これが例のAPIです。
見るとデータシートのコードから修正され、除算演算子を使った計算に置き換えられています。
実際は2^nの除算しか使ってないので、コンパイラの腕の見せ所(最適化でシフト処理に置き換えられるか)ですが、さすがに処理系依存のコードを書くわけにもいかないのでこの辺が妥当でしょう。

生の測定値とはいったい何を表している値なのか、この演算処理はどういった計算をしているのかはデータシートでの説明が皆無ですので置いておくとして、ここで重要なことは気圧や湿度の変換には温度データが必要な点です。上記APIには「t_fine」という変数に温度が保存され、気圧、湿度の変換関数で使われていますが、このt_fineには温度[℃]の値が5120倍された数値になっていて保存されています。
その点に注意しながら上記APIのコードを使いましょう。

これで無事測定ができるようになりました。


これは、外出から帰ってきたときからの部屋の温湿度・気圧の推移です。最初は32℃くらい室温がありましたが、エアコンを入れたので28℃くらいまで下がっている様子がわかります。湿度もエアコンによって下がっていますね。


今回はBME280に絞った記事ですが、実際は先日のMPU-9250と同時にデータを取っています。
もうちょっとプログラムに磨きを掛けて、細かな完成度を上げていきたいです。

2017年8月14日月曜日

MPU-9250をPIC32MX250F128B+Harmonyで使う

前回の記事でUSBメモリーの読み書きをやりました。
さて、なぜこれをやったかというと、ロガー的なものを作ってみたかったからなんですね。

今回は、9軸モーションセンサであるInvenSense製MPU-9250をいじってみようと思います。

MPU-9250はジャイロセンサ、加速度センサ、地磁気センサの3つのセンサが搭載されているセンサで、I2CまたはSPIでその値を読みだすことができます。
ほかにもDMP(Digital Motion Processor)という計算チップが入っており、これら3つのセンサの値をから姿勢を算出し、オイラー角や四元数などの値を吐き出すこともできるようです。

今回は、とりあえずとっかかりとしてI2C接続でセンサの吐き出す加速度と角速度、そしておまけで付いてくる温度を読み出してUSBメモリーに記録していきたいと思います。

ちなみに、なぜ地磁気センサを読み出さないかというと、MPU-9250の中で別チップになっているからです。MPU-9250の中には加速度センサやジャイロセンサとは独立して、完全に別チップ(旭化成製AK8963)として地磁気センサが入っています。そのため、加速度センサの値等とは完全に別枠で制御してやる必要があります。なので、これはまた今度の課題とします。


さて、細かなスペックはデータシートレジスターマップを見ればだいたいわかります。
ですので、どんどんコードを書いていきたいと思います。

typedef struct {
    DRV_HANDLE hDriver;
    DRV_I2C_BUFFER_HANDLE hBufferDriver;
    uint8_t WriteBuffer[2];    
    uint8_t SlaveAddress;
} MPU9250;

まずは、MPU-9250にアクセスする用のデータをストックする構造体を作っておきます。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool MPU9250_Init(MPU9250 *pobj, SYS_MODULE_INDEX index, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return pobj->hDriver != (DRV_HANDLE)NULL;
}

次は初期化の関数です。特に変なところは無いと思います。
I2Cの送受信が終わったときなどにそのイベントを受け取る関数を一応用意していますが、今回は使わないので中身は空にしています。

DRV_I2C_BUFFER_EVENT MPU9250_GetTransferStatus(MPU9250 *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool MPU9250_IsI2CCommunicating(MPU9250 *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = MPU9250_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

I2Cの状態を確認するラッパー関数です。
MPU9250_IsI2CCommunicating関数は、これがTRUEを返すと現在送受信中で、FALSEを返すと現在アイドル中であることを表します。

_Bool MPU9250_StartWritingResistor(MPU9250 *pobj, uint8_t address, uint8_t data)
{
    if(MPU9250_IsI2CCommunicating(pobj))
        return false;
    
    pobj->WriteBuffer[0] = address;
    pobj->WriteBuffer[1] = data;
    
    pobj->hBufferDriver = DRV_I2C_Transmit(pobj->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 2, NULL);
    
    return pobj->hBufferDriver != NULL;
}

_Bool MPU9250_StartReadingResistor(MPU9250 *pobj, uint8_t address, uint8_t *pdata)
{
    return MPU9250_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool MPU9250_StartBurstReadingResistor(MPU9250 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(MPU9250_IsI2CCommunicating(pobj))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->hBufferDriver != NULL;
}

これが実際に送受信をする関数です。

注意しなければならないことは、スレーブアドレスはそのまま入れられないということです。
I2Cは最初の8bit中上位7bitでスレーブアドレスを送り、下位1bitでリードモードかライトモードかを指定します。
Harmonyのドライバ関数では、そのリード/ライトのビット付加は自動でやってくれますが、アドレスの1bitシフトはやってくれません。ですので、スレーブアドレスは左に1bitシフトしたうえで渡す必要があります。

また、これらの関数は読み書きをしたいメモリアドレスをパラメーターとして受け取りますが、DRV_I2C_TransmitThenReceive関数などでは内部でこの送信内容のデータのポインタを保持しています。ですので、この関数から制御が戻ると値が不定となるスタック領域ではだめで、MPU9250構造体の中に確保したメモリにアドレスをコピーしてそれを参照するようにしています。

uint16_t MPU9250_ParseUInt16(uint8_t *pData)
{
    return (uint16_t)pData[0] << 8 | pData[1];
}

int16_t MPU9250_ParseInt16(uint8_t *pData)
{
    return (int16_t)MPU9250_ParseUInt16(pData);
}

void MPU9250_ParseVector(uint8_t *pData, VECTOR_3D *pOut)
{
    pOut->X = MPU9250_ParseInt16(pData);
    pOut->Y = MPU9250_ParseInt16(pData + 2);
    pOut->Z = MPU9250_ParseInt16(pData + 4);
}

あとはユーティリティ的なものですが、MPU-9250は16bit値は上位8bitのほうが若番のアドレスが振られています。多くの処理系とは異なる(とか言うと怒られそうですが)ので、手動でシフト演算するように作っています。

さて、このライブラリを使う側(app.c)は、だいたいこんな実装になります。

case APP_STATE_TMR_I2C_INIT:
    //Initialize I2C and Send 'Who am I' command
    if(MPU9250_Init(&appData.mpu9250, DRV_I2C_INDEX_0, MPU9250_I2C_ADDRESS_0)) {
        MPU9250_StartReadingResistor(&appData.mpu9250, MPU9250_RA_WHO_AM_I, RXBuffer);
        appData.state = APP_STATE_MPU9250_CHECK_WHO_AM_I;
    } else
        appData.state = APP_STATE_ERROR;

    break;            
case APP_STATE_MPU9250_CHECK_WHO_AM_I:            
    switch(MPU9250_GetTransferStatus(&appData.mpu9250)) {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            if(RXBuffer[0] == MPU9250_RESPONSE_WHO_AM_I)
                appData.state = APP_STATE_READ_RAW_DATA;
            else
                appData.state = APP_STATE_ERROR;
            break;                    
        case DRV_I2C_BUFFER_EVENT_ERROR:
            appData.state = APP_STATE_ERROR;
            break;
        default:
            break;
    }            
    break;            
case APP_STATE_READ_RAW_DATA:
    if(MPU9250_StartBurstReadingResistor(&appData.mpu9250, MPU9250_RA_ACCEL_XOUT_H, RXBuffer, 14)) {
        i2cWaitCounter = 0;
        appData.state = APP_STATE_CHECK_RAW_DATA;
    } else {
        appData.state = APP_STATE_ERROR;
    }
    break;            
case APP_STATE_CHECK_RAW_DATA:
{
    DRV_I2C_BUFFER_EVENT event = MPU9250_GetTransferStatus(&appData.mpu9250);
    switch(event) {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            MPU9250_ParseVector(&RXBuffer[0], &appData.accel);
            appData.temperature = MPU9250_ParseInt16(&RXBuffer[6]);
            MPU9250_ParseVector(&RXBuffer[8], &appData.gyro);
            
            appData.state = APP_STATE_READ_RAW_DATA;

            break;                    
        case DRV_I2C_BUFFER_EVENT_ERROR:
            appData.state = APP_STATE_ERROR;
            break;
        default:    //I2C communication in operating
            if(i2cWaitCounter++ > 1000) {
                appData.state = APP_STATE_ERROR;
            }
            break;
    }
    break;
}            

まずはWhoAmIコマンドを送り、MPU-9250がちゃんと機能しているかを確認します。
その次は、加速度センサのx軸の上位8bitから14bytes連続で読みだします。ここに加速度センサの各軸の値、温度、ジャイロセンサの各軸の値が含まれます。
I2Cの読み込みが終了したら、値を分離して保存してあげます。
デフォルトで加速度は±2gFSですので、測定値の生の値を2^(16-2)で割ってから重力加速度の9.8を掛けてあげればm/s^2になります。


測定データの加速度の値です。ロギング中に基板をグリグリと回してあげたのでこのような測定結果が得られています。静置時にz軸方向におよそ9.8m/s^2が出ているので、値としてもおかしくないと思います。



…とまあここまですんなりと書きましたが、ハマりポイントはありました。

実際はこのI2C通信以外にもUSBメモリーの書き込み処理や、時間を計測するためのタイマー処理などをやっているわけで、コードは結構複雑になっています。
そんな中、データを取っていると0~1秒程度でハングアップしてしまう現象にかなり悩まされました。
I2Cがハングアップしているのか、USBがハングアップしているのか、LEDデバッグという超縛りプレイ環境のせいで切り分けに苦労しましたが 、結局はI2Cがハングアップしていました。

不具合の内容としては、上記プログラムの中でMPU9250_GetTransferStatus関数を呼び出し、I2Cの通信が完了するのを待つというところがありますが、いつまでたってもI2Cの通信が終わらないというものでした。
ですが、I2Cの不具合となれば、USBメモリーの読み書き機能が使えますので、実際にMPU9250_GetTransferStatus関数(DRV_I2C_TransferStatusGet関数)が返すDRV_I2C_BUFFER_EVENTの値と、DRV_I2C_BytesTransferred関数が返す送受信済みバイト数を記録してみました。


青線がDRV_I2C_BUFFER_EVENTの値で橙線が送受信済みバイト数です。
レジスタアドレス送信+14bytesリードで計15bytesの読み書きが行われています。
statusとしては正常に送信要求→リスタート→ACK送信の流れで読み書きができていますが、最後は5bytes分読み込んだところでブツンと受信が息絶えてしまっています。

そもそもI2Cはマスター側がクロックを発生させて、そのタイミングでスレーブがデータを送信しますので、スレーブがそれに反応できなかったとしてもプルアップ抵抗のために0xFFのデータが受かるだけです。そのため、受信中にハングアップするのは、明らかにスレーブではなくマスターのせいなわけです。


さてさて、こうなるとHarmonyのI2CドライバがDRV_I2C_TransmitThenReceive関数の呼び出しを受けて処理を開始し、各タイミングで発生する割り込みを処理し、送受信を完了させるまでの間にどこか問題があるということになって、一ユーザーがどうにかできるような領域じゃなくなってきてしまいます。困った困った。

ん…?各タイミングで発生する割り込み処理…?


設定を見ると、デフォルトでI2Cの割り込み優先度が1になっています。
タイマやUSBは4に設定されており、I2C割り込み中にもUSBの割り込みが発生することができます。

I2Cの割り込み処理をしている間に他の割り込みが入ってきて、受信データの処理が間に合わなくなったらどうなる…?ACKを送るタイミングを逃したらNACKだとスレーブが勘違いして送るのをやめるのでは…?

というわけで、優先度を最大の7にしてみました。
そうすると、無事、安定して動きましたとさ。



ふぅ、これで3日間くらい格闘してたぜ…。
これでやっと次のステップに移れる…。

2017年8月11日金曜日

PIC32MX250F128B+HarmonyでUSBメモリーに書き込みをする

やはりUSB Hostを手軽にやりたいならばPIC32MX250F128Bです。
以前、USBメモリーにアクセスしたり、 それを一部改造してファイルシステムはFatFsで読み書きしたりして遊んでいましたが、久々にUSBメモリーを使いたくなってもう一度この時使ったPIC32MX250を取り出してきました。

当時とは環境を一新してやろうとしたところ、どうもライブラリに新しいものが出てきているようでした。

MPLAB Harmony

MPLAB HarmonyはPIC32シリーズ専用のライブラリ群(公式サイトにはフレームワークって書いてありますが)で、柔軟で抽象度の高いプログラムを書くことを目的に開発されたもののようです。Harmonyという名前からも、各機能が調和し合って完成していくような様を表しているのでしょう。

以前、PIC32MX250でUSBメモリーにアクセスしたときはMicrochip Libraries for Application(MLA)というライブラリを使っておりました。MicrochipはこれをMPLAB Harmonyで置き換えようとしているみたいで、新しいバージョンのMPLAB XやXC32でMLAを使おうとしたところ鬱陶しいほどの警告が出てきました。


というわけで、今回はMPLAB Harmonyを使ってUSBメモリーにアクセスしてみたいと思います。回路はMLAのときとほぼ同じですが、LEDの場所だけ都合によって変えました。

とその前に、今回使用した環境だけ明記しておきますね。こういうの、バージョンによって結構変わってきそうなので。
  • MPLAB X IDE v3.65
  • XC32 v1.44
  • MPLAB Harmony v1.11
まずは、HarmonyをインストールしてHarmonyのプロジェクトを作れるようにします。これはググればいろいろなサイトで出てくるのでそこまで苦労しないでしょう。


プロジェクトテンプレートが用意されているので、今までのようにサンプルプログラムから必要なソースコードをコピペしてきたリ、いちいちライブラリのincludeディレクトリに参照を追加したりという面倒な作業が必要無くなります。

そして何よりも強力なのがこの「MPLAB Harmony Configurator」です。
PICでプログラミングするうえで、初心者殺し(そして慣れれば単に面倒)だった作業の1つであるConfigurationビットの設定がGUIでできてしまいます。そして、Configurationビットだけにとどまらず、どのライブラリをインポートするか、どのような設定で使うかなどということまでここで設定できてしまうためかなり楽ちんになっています。


今回はUSB HostでMSD(Mass Storage Device)を使うので上記のような設定にします。
そして、Harmony Configuratorで最も強力なのがこのClock Diagramです。


複雑な構造のクロックのプリスケーラ、ポストスケーラ等の設定が図の上で簡単に設定できてしまいます。
今回は8MHzのセラロックを付けたので、そのように設定し、USBには48MHz、システムクロックには60MHzが供給できるように設定します。

最後に「Pin Settings」でLEDを接続したピンをデジタル出力に設定して、「Generate Code」ボタンをクリックすれば自動的にこの設定に従ったソースコードをプロジェクトに設定してくれます。
ああ、なんとゆとりな時代になったのでしょう。

ちなみに、ユーザーが編集するソースコードは[ProjectName]/Source Files/app/app.cと[ProjectName]/Header Files/app/app.hの2つだけです。
これ以外は上記のGeneratorが自動的に作ってくれるコードですので、設定を変えたらせっかく自分が編集していた分も消え去ってしまいます。

おっと、最後に1つ注意事項。
ヒープ領域を設定します。
これを設定しないと、謎の実行時エラー(USB_HOST_EVENT_DEVICE_UNSUPPORTED)に悩まされることになります。これはMLAの時もハマりポイントでした。適当に2000bytesくらいをヒープ領域に割り当てておけばよいでしょう。



さて、肝心のapp.cとapp.hですが、これはデモプログラムの移植がとっつきやすいでしょう。
Harmonyのインストールフォルダ(C:\microchip\harmony\v1_11\apps\usb\host\msd_basic\firmware\src)の中にあるapp.cとapp.hを今作ったプログラムのapp.cとapp.hにコピペします。

コンパイルするとLED関係の関数呼び出し(マイコンボード用?)がエラーになりますが、必須ではないのでコメントアウトしてやれば良いでしょう。


ちなみにですが、今回はRB13にLEDを付けましたので、アクセスランプとして書き込み中に点灯するように改造しておきます。
case APP_STATE_WRITE_TO_FILE:

    // Try writing to the file
    
    PLIB_PORTS_PinSet(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_13);
    
    if (SYS_FS_FileWrite( appData.fileHandle, "Hello, world!\r\n", 15 ) == -1)
    {
        // Write was not successful. Close the file and error out.
        SYS_FS_FileClose(appData.fileHandle);
        appData.state = APP_STATE_ERROR;

    }
    else
    {
        // We are done writing. Close the file
        appData.state = APP_STATE_CLOSE_FILE;
    }

    PLIB_PORTS_PinClear(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_13);
    
    break;
GPIOの制御はPLIB_PORTS_PinSet関数などを使います。
直接LATBなどを叩いていたらライブラリによる抽象化が台無しですからね。

余談ですが、Harmonyはファイルシステムの制御モジュールとしてFatFsを使っているようです。
ついにMicrochipに使われるようになったかFatFs。素晴らしいライブラリです。ですので、MLAのときにあったタイムスタンプが書きこまれないトラブルも回避されています。


さて、前回はUSBメモリーへの書き込みをやっただけで満足して終わりでしたが、今回はちゃんとアプリケーションを作るぞー!