STM32/HALでTSYS01を使ってみた

## TSYS01

　ストロベリーリナックスで販売している温度センサ。

　[TSYS01 0.1℃精密温度センサモジュール - TSYS01 - ネット販売](https://strawberry-linux.com/catalog/items?code=80018)

　比較的安価(税抜き980円)でありながら、-5 - +50℃の範囲で±0.1℃と高精度な温度センサ。

　動作電圧は2.2Vから3.6Vだが、高精度で計測するには3.2Vから3.4Vと比較的狭い範囲が指定されている。
　バスはSPIとI2Cを選択することができる。

　詳しくはデータシートを参照のこと。

## テスト環境

　STBee F4mini(168MHz動作)と組み合わせ、I2Cバス(100kHz)で使用した。

## ライブラリ

　STM32 HALとCMSIS-RTOS APIに依存している。

### 使い方

#### コンストラクタ

```c++
TSYS01(I2C_HandleTypeDef &hi2c, uint8_t device_address);
```

　引数に、通信に使用するI2Cのハンドラとセンサのアドレスを指定する。
　静的メンバ定数で`address_CSB_high`と`address_CSB_low`が定義されているので、基本的にこのどちらかを指定する。
　ストリナの説明書に書いてある使い方なら`address_CSB_high`を指定すればいい。

#### 初期化

　コンストラクタではメンバ変数の初期化のみを行う。
　実際に使う際にはデバイスとインスタンスの初期化を行う必要がある。初期化には`initialize`を使用する。

```c++
bool initialize(void);
bool reset(void);
```

　`initialize`の中では最初にリセットコマンドが送信される。
　`reset`は、NACKな(デバイスが存在しない or アドレスを間違っている)場合は`false`を返す。
　`initialize`から呼んだ`reset`が`false`を返した場合は、`initialize`も`false`を返して終了する。

　`reset`が成功した場合は続けてPROMの読み出しを行う。
　PROMの読み出しが成功した場合(すべての読み出しでACKを受信した場合)はPROMのチェックサムの確認を行う。
　TSYS01のチェックサムは、すべてのPROMのデータ(16byte)を加算した下位2桁が0x00になるように設定されている。加算した結果の下位2桁が0x00に一致しない場合、`initialize`は`false`を返す。
　チェックサムの確認に成功した場合はPROMをprivateの配列に保存して、`true`を返して終了する。

#### 読み出し

```c++
bool conversion(void);
bool read_result(uint32_t &result_raw);

bool get_coefficient(uint16_t &k4,
                        uint16_t &k3,
                        uint16_t &k2,
                        uint16_t &k1,
                        uint16_t &k0);

bool convert_from_raw(uint32_t raw, float &result);
bool convert_from_raw(uint32_t raw, double &result);

bool read_sequence(float &result);
bool read_sequence(double &result);
```

　簡単に使うには、`read_sequence`を使用する。

　`read_sequence`の中では`conversion`、`read_result`、`convert_from_raw`を呼び出し、気温を返す。
　気温は摂氏で返される。

***

　`conversion`は変換開始コマンドを送信し、ACKを受け取った場合は一定時間(ヘッダで定義したミリ秒)を待機後に`true`を返す。NACKを受け取った場合は即時`false`を返す。

　`read_result`は変換結果レジスタの3バイトを読み出し、u32bitとして返す。ACKなら`true`、NACKなら`false`を返す。

　`convert_from_raw`は引数に与えられたrawデータを、`initialize`で読み出した係数を使用して摂氏に変換する。係数が正しく読めていない場合は`false`を返し、それ以外は`true`を返す。

　`convert_from_raw`は引数が`float`と`double`で多重定義されている。それぞれ内部で`float`を使うか`double`を使うかの違い。
　STM32F3/F4は単精度FPUを搭載しているため、`float`を使用すると高速に計算できる。`double`を使うと速度を犠牲にする代わりに精度を達成できる。もっとも、簡単に比較した限りでは`float`と`double`の違いは十分誤差の範囲だった。

　`read_sequence`も多重定義されているが、これは内部で呼び出す`convert_from_raw`の違いによる。

***

　`get_coefficient`を使用することにより、メンバ変数に記録された係数を読み出すことができる。

#### 動作テスト

　一旦raw値を読み出し、それを`convert_from_raw`で計算する。
　また`float`と`double`の2種類の変換を行い、変換時間を計測する。

```c++
const bool tsys01_init_status = tsys01_instance.initialize();
printf("init: %s\n", tsys01_init_status ? "OK" : "ERR");

if (tsys01_init_status)
{
    bool status = true;

    status = status && tsys01_instance.conversion();

    uint32_t result_raw = 0;
    status = status && tsys01_instance.read_result(result_raw);

    printf("raw: %lu\n", result_raw);

    uint16_t k4 = 0;
    uint16_t k3 = 0;
    uint16_t k2 = 0;
    uint16_t k1 = 0;
    uint16_t k0 = 0;

    status = status && tsys01_instance.get_coefficient(k4, k3, k2, k1, k0);

    if (status)
    {
        printf("k4: %hu\n", k4);
        printf("k3: %hu\n", k3);
        printf("k2: %hu\n", k2);
        printf("k1: %hu\n", k1);
        printf("k0: %hu\n", k0);

        bool tmp_b = false;

        float tmp_f = 0;

        portENTER_CRITICAL();
        { // measure convert clock
            HAL_TIM_GenerateEvent(&htim7, TIM_EVENTSOURCE_UPDATE);
            htim7.Instance->CR1 |= TIM_CR1_CEN;

            tmp_b = tsys01_instance.convert_from_raw(result_raw, tmp_f);

            htim7.Instance->CR1 &= ~TIM_CR1_CEN;
        }
        portEXIT_CRITICAL();

        if (tmp_b)
        {
            printf("f: %+10.6f deg C (%lu)\n", tmp_f, htim7.Instance->CNT);
        }

        double tmp_d = 0;

        portENTER_CRITICAL();
        { // measure convert clock
            HAL_TIM_GenerateEvent(&htim7, TIM_EVENTSOURCE_UPDATE);
            htim7.Instance->CR1 |= TIM_CR1_CEN;

            tmp_b = tsys01_instance.convert_from_raw(result_raw, tmp_d);

            htim7.Instance->CR1 &= ~TIM_CR1_CEN;
        }
        portEXIT_CRITICAL();

        if (tmp_b)
        {
            printf("d: %+10.6f deg C (%lu)\n", tmp_d, htim7.Instance->CNT);
        }
    }
}
```

　以下のような結果になった

```
init: OK
raw: 9710806
k4: 5714
k3: 7338
k2: 15996
k1: 22585
k0: 34027
f: +22.522125 deg C (68)
d: +22.522079 deg C (1018)
```

　fがfloatで計算した気温、dがdoubleで計算した気温。十分に一致している。

　変換には、`float`で68クロック、`double`で1018クロックかかった。
　コアは168MHz動作だが、タイマは84MHzで動作している。つまり`float`では800ナノ秒、`double`では12マイクロ秒程度かかっている、ということになる。
　`float`でも十分な精度があるから、わざわざ`double`を使う必要はないはず。

#### 連続計測

```c++
uint32_t prev_work = 0;
uint32_t interval = 1000;

while (1)
{
    osDelayUntil(&prev_work, interval);

    const uint32_t tick = osKernelSysTick();

    float temperature = 0;
    if (!tsys01_instance.read_sequence(temperature))
    {
        temperature = std::numeric_limits<float>::quiet_NaN();
    }

    printf("%.3f %+8.4f\n", tick * 1e-3f, temperature);
}
```

　1Hzで計測する場合は上記のようになる。
　`read_sequence`で`false`が帰った場合はNaNを表示するようにしている。
　例えばI2Cバスが切断された場合などはNaNとなるが、ノイズ等で不正な値を読んでもNaNにはならない(温度の値にはチェックサムが無いため)。
　

### ソースコード

#### ヘッダ

```c++:Device_driver/TSYS01.h
#ifndef Device_driver__TSYS01__H
#define Device_driver__TSYS01__H

#include <stm32f4xx.h>

namespace Device_driver
{
class TSYS01
{
  public: // portable

  private: // portable
    static constexpr uint32_t I2C_timeout = 50;

    static constexpr uint32_t delay_after_reset = 5;
    static constexpr uint32_t delay_after_conversion = 10;

    static constexpr uint8_t PROM_size = 8;
    static constexpr uint8_t result_size = 3;

  public:
    enum class Register_address : uint8_t
    {
        reset = 0x1E,
        start_ADC_temperature_conversion = 0x48,
        read_ADC_temperature_result = 0x00,
        PROM_read_address_0 = 0xA0,
        PROM_read_address_1_coefficient_k4 = 0xA2,
        PROM_read_address_2_coefficient_k3 = 0xA4,
        PROM_read_address_3_coefficient_k2 = 0xA6,
        PROM_read_address_4_coefficient_k1 = 0xA8,
        PROM_read_address_5_coefficient_k0 = 0xAA,
        PROM_read_address_6_SN_high = 0xAC,
        PROM_read_address_7_SN_low_and_checksum = 0xAE,
    };

    static constexpr uint8_t address_CSB_high = 0x76 << 1;
    static constexpr uint8_t address_CSB_low = 0x77 << 1;

    TSYS01(I2C_HandleTypeDef &hi2c, uint8_t device_address);
    ~TSYS01(void);

    bool initialize(void);

    bool reset(void);

    bool conversion(void);
    bool read_result(uint32_t &result_raw);

    bool get_coefficient(uint16_t &k4,
                         uint16_t &k3,
                         uint16_t &k2,
                         uint16_t &k1,
                         uint16_t &k0);

    bool convert_from_raw(uint32_t raw, float &result);
    bool convert_from_raw(uint32_t raw, double &result);

    bool read_sequence(float &result);
    bool read_sequence(double &result);

  protected:
  private:
    I2C_HandleTypeDef *const hi2c;
    const uint8_t device_address;

    bool is_correct_PROM_data;
    uint16_t PROM_data[PROM_size];

    bool read_PROM(void);
    static bool check_PROM_checksum(const uint16_t (&PROM_data)[PROM_size]);

    bool read_register(Register_address register_address,
                       uint8_t *buff,
                       uint8_t length);
    bool write_register(Register_address register_address,
                        uint8_t *buff,
                        uint8_t length);

    bool read_register(Register_address register_address, uint16_t &result);
};
} // namespace Device_driver

#endif
```

#### 実装

```c++:Device_driver/TSYS01.cpp
#include <Device_driver/TSYS01.h>
#include <cmsis_os.h>

namespace Device_driver
{
TSYS01::TSYS01(I2C_HandleTypeDef &hi2c, uint8_t device_address)
    : hi2c(&hi2c),
      device_address(device_address),
      is_correct_PROM_data(false),
      PROM_data()
{
    static_assert(PROM_size == 8, "PROM size error");
    static_assert(result_size == 3, "result size error");
}

TSYS01::~TSYS01(void)
{
}

bool TSYS01::initialize(void)
{
    bool status = true;

    status = status && reset();
    status = status && read_PROM();

    return (status);
}

bool TSYS01::reset(void)
{
    const bool status = write_register(Register_address::reset, 0, 0);

    if (status)
    {
        osDelay(delay_after_reset);
    }

    return (status);
}

bool TSYS01::conversion(void)
{
    const bool status = write_register(Register_address::start_ADC_temperature_conversion, 0, 0);

    if (status)
    {
        osDelay(delay_after_conversion);
    }

    return (status);
}

bool TSYS01::read_result(uint32_t &result_raw)
{
    uint8_t buff[result_size] = {};

    const bool status = read_register(Register_address::read_ADC_temperature_result, buff, result_size);

    if (status)
    {
        uint32_t result = 0;

        for (uint32_t i = 0; i < result_size; i++)
        {
            result |= buff[i] << (result_size - i - 1) * 8;
        }

        result_raw = result;
    }

    return (status);
}

bool TSYS01::get_coefficient(uint16_t &k4,
                             uint16_t &k3,
                             uint16_t &k2,
                             uint16_t &k1,
                             uint16_t &k0)
{
    // exit return

    if (is_correct_PROM_data)
    { // return

        k4 = PROM_data[1];
        k3 = PROM_data[2];
        k2 = PROM_data[3];
        k1 = PROM_data[4];
        k0 = PROM_data[5];

        return (true);
    }

    return (false);
}

bool TSYS01::convert_from_raw(uint32_t raw, float &result)
{
    // exit return

    if (!is_correct_PROM_data)
    { // return

        return (false);
    }

    const float k4 = PROM_data[1];
    const float k3 = PROM_data[2];
    const float k2 = PROM_data[3];
    const float k1 = PROM_data[4];
    const float k0 = PROM_data[5];

    const float ADC16 = raw / 256.0f;

    result = -2.0f * k4 * 1e-21f * (ADC16 * ADC16 * ADC16 * ADC16) +
             +4.0f * k3 * 1e-16f * (ADC16 * ADC16 * ADC16) +
             -2.0f * k2 * 1e-11f * (ADC16 * ADC16) +
             +1.0f * k1 * 1e-6f * (ADC16) +
             -1.5f * k0 * 1e-2f;

    return (true);
}

bool TSYS01::convert_from_raw(uint32_t raw, double &result)
{
    // exit return

    if (!is_correct_PROM_data)
    { // return

        return (false);
    }

    const double k4 = PROM_data[1];
    const double k3 = PROM_data[2];
    const double k2 = PROM_data[3];
    const double k1 = PROM_data[4];
    const double k0 = PROM_data[5];

    const double ADC16 = raw / 256.0;

    result = -2.0 * k4 * 1e-21 * (ADC16 * ADC16 * ADC16 * ADC16) +
             +4.0 * k3 * 1e-16 * (ADC16 * ADC16 * ADC16) +
             -2.0 * k2 * 1e-11 * (ADC16 * ADC16) +
             +1.0 * k1 * 1e-6 * (ADC16) +
             -1.5 * k0 * 1e-2;

    return (true);
}

bool TSYS01::read_sequence(float &result)
{
    bool status = true;

    status = status && conversion();

    uint32_t raw = 0;
    status = status && read_result(raw);

    status = status && convert_from_raw(raw, result);

    return (status);
}

bool TSYS01::read_sequence(double &result)
{
    bool status = true;

    status = status && conversion();

    uint32_t raw = 0;
    status = status && read_result(raw);

    status = status && convert_from_raw(raw, result);

    return (status);
}

bool TSYS01::read_PROM(void)
{
    uint16_t buff[PROM_size] = {};

    const bool read_PROM_status =
        read_register(Register_address::PROM_read_address_0, buff[0]) &&
        read_register(Register_address::PROM_read_address_1_coefficient_k4, buff[1]) &&
        read_register(Register_address::PROM_read_address_2_coefficient_k3, buff[2]) &&
        read_register(Register_address::PROM_read_address_3_coefficient_k2, buff[3]) &&
        read_register(Register_address::PROM_read_address_4_coefficient_k1, buff[4]) &&
        read_register(Register_address::PROM_read_address_5_coefficient_k0, buff[5]) &&
        read_register(Register_address::PROM_read_address_6_SN_high, buff[6]) &&
        read_register(Register_address::PROM_read_address_7_SN_low_and_checksum, buff[7]);

    const bool correct_PROM_checksum = read_PROM_status && check_PROM_checksum(buff);

    is_correct_PROM_data = false;

    for (uint8_t i = 0; i < PROM_size; i++)
    {
        PROM_data[i] = buff[i];
    }

    is_correct_PROM_data = correct_PROM_checksum;

    return (correct_PROM_checksum);
}

bool TSYS01::check_PROM_checksum(const uint16_t (&PROM_data)[PROM_size])
{
    const uint8_t *const ptr = reinterpret_cast<const uint8_t *>(PROM_data);
    uint32_t total = 0;

    for (uint32_t i = 0; i < PROM_size * sizeof(uint16_t); i++)
    {
        total += ptr[i];
    }

    const bool result = (total & 0xFF) == 0x00;

    return (result);
}

bool TSYS01::read_register(const Register_address register_address,
                           uint8_t *const buff,
                           const uint8_t length)
{
    return (HAL_OK == HAL_I2C_Mem_Read(hi2c,
                                       device_address,
                                       static_cast<uint16_t>(register_address),
                                       I2C_MEMADD_SIZE_8BIT,
                                       buff,
                                       length,
                                       I2C_timeout));
}

bool TSYS01::write_register(const Register_address register_address,
                            uint8_t *const buff,
                            const uint8_t length)
{
    return (HAL_OK == HAL_I2C_Mem_Write(hi2c,
                                        device_address,
                                        static_cast<uint16_t>(register_address),
                                        I2C_MEMADD_SIZE_8BIT,
                                        buff,
                                        length,
                                        I2C_timeout));
}

bool TSYS01::read_register(const Register_address register_address, uint16_t &result)
{
    uint8_t tmp[2] = {};
    const bool status = read_register(register_address, tmp, sizeof(tmp));

    if (status)
    {
        result = ((uint16_t)tmp[0] << 8 | tmp[1]);
    }

    return (status);
}

} // namespace Device_driver
```