TSYS01
ストロベリーリナックスで販売している温度センサ。
TSYS01 0.1℃精密温度センサモジュール - TSYS01 - ネット販売
比較的安価(税抜き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に依存している。
使い方
コンストラクタ
TSYS01(I2C_HandleTypeDef &hi2c, uint8_t device_address);
引数に、通信に使用するI2Cのハンドラとセンサのアドレスを指定する。
静的メンバ定数でaddress_CSB_high
とaddress_CSB_low
が定義されているので、基本的にこのどちらかを指定する。
ストリナの説明書に書いてある使い方ならaddress_CSB_high
を指定すればいい。
初期化
コンストラクタではメンバ変数の初期化のみを行う。
実際に使う際にはデバイスとインスタンスの初期化を行う必要がある。初期化にはinitialize
を使用する。
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
を返して終了する。
読み出し
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種類の変換を行い、変換時間を計測する。
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
を使う必要はないはず。
連続計測
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にはならない(温度の値にはチェックサムが無いため)。
ソースコード
ヘッダ
# 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
実装
# 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