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メモリーへの書き込みをやっただけで満足して終わりでしたが、今回はちゃんとアプリケーションを作るぞー!

2017年5月29日月曜日

パナソニックの照明を制御する

我が家の建物にはパナソニック製の照明が備え付けられています。
そして、入居してしばらくは気づかなかったのですが、どうもこの照明、リモコンに対応しているようです。部屋にはリモコンが備え付けられていませんでした。

型番をググって説明書を見ると、リモコンはHK9487MMというものらしいです。Amazonで2500円程度…。高いな…。

さて、説明書やリモコンの画像を見ると、どうも下記のボタンがあるようです。
  • 点灯(普段)
  • 全灯
  • 常夜灯
  • 消灯
  • 白い色(調色ボタン)
  • 暖かい色(調色ボタン)
  • 明るい(明暗ボタン)
  • 暗い(明暗ボタン)
  • おやすみ30(30分タイマー)
  • チャンネル選択(1,2,3)
  • チャンネル確定
ほうほう、壁のボタンだと全灯→暗め→常夜灯しか切り替えられませんが、こんなにいろいろな機能があるようです。

手元にリモコンがあれば、赤外線フォトトランジスタとかを使ってコードを解析すればいいのですが、無いのでどうしようもありません。

赤外線通信の方式

どうもググるところによると、この手の赤外線通信はおおよそ3種類あるそうです。NECフォーマット、SONYフォーマット、家電協フォーマットがその3種類ですが、パナソニック製照明はどうも家電協フォーマットのようです。

赤外線リモコンの通信フォーマット

細かいプロトコルはこのページに詳しく書いてあります。
T=425usとして、赤外線LEDを38kHzで点滅させている状態(duty比1:2)をON、消灯させている状態をOFFとすると、0はONを1T、OFFを1Tで1はONを1T、OFFを3Tで表現するようです。

ここでハマったことは、最後のビットを送信し終わった後にONを1Tだけ送ってストップビットとすることです。このストップビットを送らないと、最後のビットでOFFが1T続いたのか3T続いたのかがわからないので、最後のビットを読むことができません。結果、デコードエラーとなり受け付けてくれません。

送信データの内容

さて、具体的にどんな信号を送ればいいかと言うと、このサイトに書いてありました。

iRemo2 リモコン・データベース

例えば、CH1の全灯コマンドは

0x344A9034A4

です。
上記のフォーマットに当てはめてみると、「0x344A」がパナソニックのカスタマーコードのようです。その次の「9」が、この16bit値を4bitずつ区切ってXORを取ったパリティです。
次は、「0x034」が命令コード本体で、最後の「A4」は、カスタマーコードのパリティを含んだ「0x9034」の16bit値を8bitずつ区切ってXORを取ったパリティのようです。

すなわち、パナソニック照明に何かしらのコマンドを送るのにキーとなるのはたったの12bit分です。
しかも、上記のページにはコマンドがある程度書いてあります。

さて、その肝になる12bit分をリストに書いてみるとこうなります。

0000 0101 0100 CH1 明るく
0000 0100 1100 CH2 明るく
0000 0101 1100 CH3 明るく
0000 1011 0100 CH1 点灯(普段)
0000 1010 1100 CH2 点灯(普段)
0000 1011 1100 CH3 点灯(普段)
0000 1101 0100 CH1 暗く
0000 1100 1100 CH2 暗く
0000 1101 1100 CH3 暗く
0000 1111 0100 CH1 消灯
0000 1110 1100 CH2 消灯
0000 1111 1100 CH3 消灯
0000 0111 0100 CH1 常夜灯
0000 0110 1100 CH2 常夜灯
0000 0111 1100 CH3 常夜灯
0000 0011 0100 CH1 全灯
0000 0010 1100 CH2 全灯
0000 0011 1100 CH3 全灯

さて、ここまで見ると(わざとらしく色分けしていますが)見えてきます。
最初の4bit、最後の3bitはよくわかりませんが、5~7bit目は点灯内容を表すコード、8,9bit目はチャンネルに対応しています。

ここまでの話はMSBからデータを送信しているとの仮定ですが、LSBから送信していたとするとチャンネルのbitがそのままチャンネル番号に対応していると言えそうですが、まあ細かいことなのであまり気にしないことにしましょう。MSBから送っていると考えたほうがわかりやすいですし。

他の送信コード

最初にリモコンの外見等から見た機能の中で、上記のコードに含まれていないものがあります。
  • 白く
  • 黄色く
  • おやすみ30(30分タイマー)
  • チャンネル確定
といったところでしょうか。
最初は上記の青色部分が「000」と「100」の2通りが無いので、これが「白く」と「黄色く」に対応するのかなと思いましたが、どうも違うようです。

とりあえず、チャンネルの位置を8,9bit目として他の部分で10bit分。これを総当たりすれば1024通りです。「白く」「黄色く」の調色ボタンは1回押しただけでは変化がわかりにくいので、20回くらい連打するべきでしょう。
20回の連打に3秒かければ、だいたい1時間くらいで総当たりが終わります。その間にゴロゴロしながらアニメでも見ていればいいでしょうと思いながらやってみました。

そうすると、一部のコードが見つかりましたが全部は見つかりませんでした。
うーん、こうなったら12bit総当たりだ!それでも4時間くらいですしまあ何とかなるでしょう。

というわけで洗いだしたコードが下記の通りになります。

1100 110‬‬1 0001 CH1 黄色
1100 1011 0001 CH2 黄色
1100 1111 0001 CH3 黄色
1100 100‬0 1001 CH1 黄色く
1100 1010 1001 CH2 黄色く
1100 100‬1 1001 CH3 黄色く
1100 010‬1 0001 CH1 白
1100 0011 0001 CH2 白
1100 0111 0001 CH3 白
1100 0000 1001 CH1 白く
1100 0010 1001 CH2 白く
1100 000‬1 1001 CH3 白く
1100 1000 0101 CH1 タイマー
1100 0101 0101 CH2 タイマー
1100 110‬0 1101 CH3 タイマー
1100 010‬1 1011 CH1 チャンネル変更
1100 110‬‬1 1011 CH2 チャンネル変更
1100 001‬‬‬1 1011 CH3 チャンネル変更

リモコンにはない、「(最も)黄色にする」「(最も)白にする」 というコマンドもオマケで見つかりました。
ですが、これを見てもどこがチャンネルと言うべきか、どこが点灯内容と言うべきかいまいちよくわからないコードです。色関係のコマンドでは少し共通性はありますが、タイマーやチャンネル変更を合わせたコマンドを見てもいまいち規則性はわかりませんでした。
誰か解読できた人がいたら教えてください。

実装(ダイジェスト)

さて、これをESP8266に実装し、基板を作成して箱に収めるとこんな感じになります。



ESP8266なのでWi-Fiで操作できます。
ウェブブラウザでも操作できますが、せっかくなのでスマホアプリも作ってみました。
まあこの辺の製作話はまた今度、気が向いたらにしましょう。

ではでは。

2017年4月23日日曜日

10万PV突破

初めての投稿から3年弱、皆様に支えられてつい最近10万PVを突破いたしました。
ありがとうございます。
趣味でやっているプログラミング関係のことを中心にしたブログですので、かなり不定期の更新になっています。下手したら更新が数か月開くこともザラでした。

ですが、最近はだいたい1日当たり100~200PVほどいただいているようです。1か月にしてだいたい4500PV程度。更新頻度が低く、しかも自分の備忘録としての側面が強いブログにしてはかなりPVが大きなほうだと思います。

参考までに、ここ1か月のPVランキングを示すと

1位WPFのScrollViewerやScrollBarのスクロール位置を同期させる2015/01/16339PV
2位ポケモン「サファイア」の時計を復活させる2016/01/22308PV
3位プロキシ設定とレジストリ2015/11/09231PV
4位WPFでタスクトレイ常駐型アプリを作る2015/11/12195PV
5位非同期処理とAggregateException2014/10/15167PV

てな具合です。

ScrollViewerの記事はここのところずーっと首位です。これがヒットするとは思っていませんでしたが、実はそれなりに需要があった記事なんですかね。
2位のポケモンのソフトのRTCをいじる記事は、最近PVをどんどん上げています。たぶん、近いうちにScrollViewerの記事を抜くのではないかなと思います(週単位でランキングを作るとすでに抜かれています)。記事を書いた当時はまだ任天堂はGBAソフトの電池の交換をやってくれていましたが、今はもうそのサービスは終了したそうなので、復活させるなら否が応でも自分でどうにかしなきゃいけないという背景もあるのかもしれません。
プロキシの記事は、自分がネットにソフトを初めて公開した件にかかわる話ですので、PVを伸ばしてくれていることはとても嬉しいです。

てな具合で、今後もぼちぼち自分のペースでブログを書いていこうと思います。
今後ともよろしくお願いいたします。

2017年4月22日土曜日

Visual Studio 2017でXamarin.Forms入門

「申し訳ないがJavaはNG」

この言葉を座右の銘に置く私としては、常にAndroidアプリ開発は戦いです。
AndroidはJava開発が最も基本で、C#で開発するにはXamarinなどの環境が必要です。

Xamarinは昔は有料だったうえ、ドキュメンテーションも少なく非常にとっつきにくいものでした。しかし、Visual Studio 2015にはオプションでXamarinをインストールできるようになり、ついに2016年にはMicrosoftに買収され、Visual Studio 2017ではモバイル開発の中核に置かれるようになりました。

というわけで、私もXamarin初心者ですが、VS2017もインストールしたことだし本格的にいじってみようと思います。

Visual Studio 2017のインストール時にXamarin関係のオプションを選択してインストールすると、プロジェクトテンプレートにXamarin.Formsが現れます。
そうするとテンプレートの選択が出てきます。
空のアプリを選択してOKを押します。
そして、プロジェクトをビルドして実行すると…
見事にエミュレーター上でAndroidアプリが動きました。

しかし、このプロジェクトテンプレートには問題が1つあります。
上の画像にも書きましたが、最初のページに相当する「MainPage.xaml」がどのプロジェクトにも含まれていません。
そのため、Visual Studioがコードの依存関係等を理解できずに、IntelliSenseがほとんど働きません。編集しているとコードのいたるところに参照が解決できずに波線が引かれます。とても鬱陶しいです。

この解決には苦労しましたが、とりあえず次のようにやるとできるようです。

まずは、ソリューションにXamarin.Forms用のPCL(Portable Class Library)を追加します。
次は各プラットフォームのプロジェクトでこのプロジェクトへの参照を追加します。
さっき追加したプロジェクトに適当なMainPage.xamlを作ります。
このテンプレートには、XAMLだけでなくコードビハインドにも余計なコードが書いてあるので注意してください。コードビハインドで架空のViewModelクラス(ContentPageViewModel)をインスタンス化しようとしてくるのでコンパイルエラーが出ます。
public partial class App : Application
{
    public App ()
    {
        InitializeComponent();

        //MainPage = new XamarinFormsTest.MainPage();
        MainPage = new XamarinFormsTest.PCL.Views.MainPage();
    }

    protected override void OnStart ()
    {
        // Handle when your app starts
    }

    protected override void OnSleep ()
    {
        // Handle when your app sleeps
    }

    protected override void OnResume ()
    {
        // Handle when your app resumes
    }
}
最後はApp.xamlのコードビハインドでMainPageのインスタンスを自分が追加してやったPCLのほうへ書き換えてやればおkです。もとあったMainPage.xamlはいらないので削除してしまってもよいでしょう。
これでMainPageをPCL上に移動することに成功しました。

PCL上にUIを置けるということは、Nugetなどを活用して適当なライブラリを入れることもできるということですので、例えばXamarin.Formsに対応しているMVVMライブラリ「Prism」を入れてみましょう。
そうすれば、ほとんどWPFと変わらずにXAML+C#でAndroidアプリを開発することができます。
このアプリでは、スライダーを 動かすとそれに追従して上に表示されている数字が変わります。「ADJUST TO 50」ボタンを押すと数字とスライダーが50の位置に移動します。
XAML+C#のデータバインディングのデモをするにはこれくらいやれば十分でしょう。

例えばコントロールの配置が「HorizontalOptions」「VerticalOptions」とかいう名前のプロパティだったりと、若干WPFと違うところもありますが、まあこれくらいならフィーリングでわかるレベルでしょう。
あとはXAMLデザイナーでリアルタイムに配置の絵が表示される機能は内容ですが、この程度ならば個人的にはまあギリギリ許容できます。

これくらいWPFの開発にそっくりだと、普段からWPFで開発している身としてはとてもありがたいですね。

というわけで、いろいろといじってみようと思います。はい。

2017年4月18日火曜日

最強のWPF MVVMインフラ「Livet」をVS2017で使う

世の中的にはWindows開発と言えばUWPとかなのかもしれませんが、まだまだレガシーなWindowsデスクトップアプリが滅びる様子はありませんよね。
そのレガシーな中でも最も近代的なプラットフォームと言えばやはりWPFで、WPFでアプリケーションを作るのならばやはりLivetは欠かせません。

Livet - ProjectHome

しかし、LivetはVS2015対応としてリリースされたver.1.3を最後に更新されておりません。
作者自身もVS2015対応版がLivet 1.xの最終リリースとする旨の発言をしており、今後はLivet2を開発すると言っています。

ugaya40/Livet2 - GitHub

しかし、現時点でLivet2のLastest commitは2015年8月。Livetの最大の武器ともいえるプロジェクトテンプレート付きのインストーラはおろか、正式リリースさえされていない状態です。このような状況ではVS2017対応は絶望的です。

そうすると、何とかして自力でLivetをVS2017にインストールしなければなりません。

さて、VS2013とVS2015がリリースされた直後も、自力でLivetのプロジェクトテンプレートを組み込んでいた漢たちはいました。

最強のWPF MVVMインフラ「Livet」をVS2013で使う
最強のWPF MVVMインフラ「Livet」をVS2015で使う

Livet自体もこの時からほとんど進んでいませんし、こちらのページで紹介されているバッチファイルをちょっと改造すればいけるでしょう。

の前に、コードスニペットを改良しておきます。
本来、その改良はVS2015対応でやるべきでした。

VS2015と同時にリリースされたC#6でnameof演算子が導入されています。この、INotifyPropertyChangedインターフェースの実装に使ってくださいと言わんばかりの機能を通知プロパティのスニペットに使わない理由は無いでしょう。
ちなみに、.NET4.5からはCallerMemberName属性を使うことで自動的に呼び出し元のプロパティ名を引数としてメソッドに引き渡す機能があるので、通知プロパティの実装にそれを使う人も多いようです(Livetももちろんそれに対応しています)。しかし、「書いてもいないものが勝手に生成される」という気持ち悪さから私はこの言語機能を使いたくないので、nameofを使ったコードを書くことにしています。

というわけで、GitHubからダウンロードしたLivet-master\Installer\Files\Snippets内のLivetProperty_NET45_CSharp.snippetは削除して、LivetProperty_NET40_CSharp.snippetをnameof演算子に書き換えておきましょう(VBも必要があればやっておくといいと思います)。
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2008/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Livet プロパティ</Title>
      <Shortcut>lprop</Shortcut>
      <Author>Livet Project</Author>
      <Description>プロパティを作成します</Description>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Imports>
        <Import>
          <Namespace>Livet</Namespace>
        </Import>
      </Imports>
      <Declarations>
        <Literal>
          <ID>type</ID>
          <ToolTip>プロパティの型</ToolTip>
          <Default>string</Default>
        </Literal>
        <Literal>
          <ID>name</ID>
          <ToolTip>プロパティ名</ToolTip>
          <Default>MyProperty</Default>
        </Literal>
      </Declarations>
      <Code Language="csharp">
<![CDATA[
#region $name$変更通知プロパティ
private $type$ _$name$;

public $type$ $name$
{
get
{ return _$name$; }
set
{ $end$
if(_$name$ == value)
return; 
_$name$ = value; 
RaisePropertyChanged(nameof($name$));
}
}
#endregion
]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>
最後に、上記のVS2015版のサイトのbatファイルの「2015」を「2017」に置換してから実行してやればOKです。
@echo off
cd %~d0%~p0

@echo on
echo copy snippets
xcopy /I /Y .\Files\Snippets\*_CSharp.* "%UserProfile%\Documents\Visual Studio 2017\Code Snippets\Visual C#\My Code Snippets\Livet"
xcopy /I /Y .\Files\Snippets\*_VB.* "%UserProfile%\Documents\Visual Studio 2017\Code Snippets\Visual Basic\My Code Snippets\Livet"

echo project templates
call :copy_project_template_csharp Livet_WPF4.0_CSharp
call :copy_project_template_csharp Livet_WPF4.5_CSharp

call :copy_project_template_vb Livet_WPF4.0_VB
call :copy_project_template_vb Livet_WPF4.5_VB

echo item templates
call :copy_item_template_csharp LivetInteractionMessageAction_CSharp
call :copy_item_template_csharp LivetMessage_CSharp
call :copy_item_template_csharp LivetModel_CSharp
call :copy_item_template_csharp LivetViewModel_CSharp
call :copy_item_template_csharp LivetWindow_CSharp

call :copy_item_template_vb LivetInteractionMessageAction_VB
call :copy_item_template_vb LivetMessage_VB
call :copy_item_template_vb LivetModel_VB
call :copy_item_template_vb LivetViewModel_VB
call :copy_item_template_vb LivetWindow_VB

echo simple install completed!!!
pause

:copy_project_template_csharp
xcopy /I /E /Y .\Files\Templates\%1 "%UserProfile%\Documents\Visual Studio 2017\Templates\ProjectTemplates\Visual C#\%1"
exit /b

:copy_project_template_vb
xcopy /I /E /Y .\Files\Templates\%1 "%UserProfile%\Documents\Visual Studio 2017\Templates\ProjectTemplates\Visual Basic\%1"
exit /b

:copy_item_template_csharp
xcopy /I /E /Y .\Files\Templates\%1 "%UserProfile%\Documents\Visual Studio 2017\Templates\ItemTemplates\Visual C#\%1"
exit /b

:copy_item_template_vb
xcopy /I /E /Y .\Files\Templates\%1 "%UserProfile%\Documents\Visual Studio 2017\Templates\ItemTemplates\Visual Basic\%1"
exit /b
これで見事VS2017に追加されました。めでたしめでたし。