前回、サンプルコードを書き込み、Lチカをトライしました。
今回は、Groveセンサでよく使われるADCとI2C入力の方法を調べてみます。開発環境はArduino IDEです。上の記事で紹介したライブラリがインストールされている前提ではなしをすすめます。
今回使用するセンサ二つ。左がアナログ、右がI2C。
M5Stamp PICO のスペック
項 目 | 内 容 |
---|---|
Controller | ESP-PICO-D4 |
Flash | 4MB |
入力電圧 | 5V@500mA |
IO | G0,G1,G3,G26,G36,G18,G19,G21,G22,G25,G32,G33 |
消費電流 | 5V@0.35-84mA |
Pinout図
pinoutは下図です。ADCの入力 G32, G33, G36にあります。I2Cのデフォルトは、G21とG22です。
Datasheet (ESP32-PICO-D4)
esp32-pico-d4_datasheet_en.pdf (espressif.com)
ADCの使い方
温度計はサーミスタなので、抵抗値の温度変化特性によって、温度を推定します。下のサンプルコードはその為の変換式がかかれていますので、複雑になってしまっていますが、基本的には、pinMode(ピン番号, input) として、analogRead(ピン番号)で信号を読むだけ。
超簡単です。
ちょうどハイライト部分です。ESP32のADCの分解能は、12bitなので、analogReadの結果は0~4095のdigit値です。
#include <math.h>
const int B = 4275; // B value of the thermistor
const int R0 = 100000; // R0 = 100k
void setup() {
Serial.begin(115200);
pinMode(32,INPUT);
}
void loop() {
int value;
value = analogRead(33);
float R = 4095.0/value-1.0;
R = R0*R;
float temperature = 1.0/(log(R/R0)/B+1/298.15)-273.15;
Serial.print("temperature = ");
Serial.println(temperature);
delay(1000);
}
下のようにSerial monitor上にセンサ値を表示すると、それっぽい値がでていることが確認できます。
これだけ。
次はI2C
使った気圧センサはInfineonのDPS310です。高精度な気圧計として、有名ですね。センサ精度が±0.002hPaで、高度にすると±0.02mを分解できるポテンシャルだそうです。ほんと?
ちなみにDPS310の評価ボードはInfineonの純正ボードの他にGroveやAdafuritから出されています。
<Wire.h>を活用する
まず、I2CはWire.hというライブラリで簡単に通信できます。まず、ボードに気圧センサをつないで、アドレスを検索してみます。
この時のスケッチは以下のとおり。
#include <Arduino.h>
#include <Wire.h>
int device_add;
int DevSearch(){
int address;
int error;
Serial.printf("\nscanning Address [HEX]\n");
for(address = 1; address < 127; address++ )
{
Wire.beginTransmission(address);
error = Wire.endTransmission();
Serial.print(".");
if(error==0)
return address;
}
return 0;
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Wire.begin(21, 22);
delay(3000);
int device_add = DevSearch();
if (0!=device_add)
{
Serial.println("");
Serial.print("Device was found ! address is 0x");
Serial.println(device_add,HEX);
}
else
{
Serial.println("");
Serial.println("Device was not found !");
}
}
void loop() {
}
下が実行結果、アドレス0x77で気圧センサのモジュールが正しく認識されています。
実際の値の読み方は、アナログ信号よりも面倒です。が、大体のセンサは、ライブラリが公開されているので、それを使うとはやいです。今回の圧力センサDPS310だと、Adafruitからライブラリが公開されてました。
自分の手持ちは、Groveの評価ボードでしたが、このライブラリでも問題なく計測できました。
下が、ライブラリ内のサンプルコードdps310_simpletestの実行結果になります。
I2Cの信号を自力でとる
ただ、ライブラリに頼りきりだと、応用がきかなかったり、ない場合は困っちゃうので、自力で値とってみます。そのためには、DPS310のデータシートを確認します。
測定の流れ
DPS310は、データシートによれば、下のような手順で、測定していくようです。
- 圧力のキャリブレ係数 c00, c10, c20, c30, c01, c21の値をレジスタから読む
- スケーリングファクタKpを下のテーブルからオーバーサンプリングの設定に応じて、選ぶ
3.信号値をレジスタから読む
4. 下の式を使って、生値をスケーリングする
5. 次に下の変換式を使って、圧力(Pa)を求める
レジスタの設定方法 (Wire.write)
手順2のスケーリングファクタを求めるためには、まず、サンプリング設定する必要があります。ここではPRS_CFGの設定を例に説明します。
データシートの中に、下画像のように気圧センサの測定条件を設定するレジスタPRS_CFGの説明があります。ここでは、測定レートを8Hz, オーバーサンプリングレートを8倍に設定します。
実際に各項目で設定を決めた後は、青字のようにビットシフトをして、指定のビットに値がおさまるようにします。
実際のコードでは、下のようにしました。まず、見た目わかりやすいようにenumをつかいます。組み込みコードではこういうのがよくでてきます。レジスタ値そのものだと全く意味不明なので、ミスも防げます。
// PM_RATEの設定
typedef enum {
DPS310_1HZ, ///< 1 Hz =000
DPS310_2HZ, ///< 2 Hz =001
DPS310_4HZ, ///< 4 Hz =010
DPS310_8HZ, ///< 8 Hz =011
DPS310_16HZ, ///< 16 Hz
DPS310_32HZ, ///< 32 Hz
DPS310_64HZ, ///< 64 Hz
DPS310_128HZ, ///< 128 Hz
} dps310_rate_t;
// PM_PRCの設定
typedef enum {
DPS310_1SAMPLE, ///< 1 Hz =000
DPS310_2SAMPLES, ///< 2 Hz =001
DPS310_4SAMPLES, ///< 4 Hz =010
DPS310_8SAMPLES, ///< 8 Hz =011
DPS310_16SAMPLES, ///< 16 Hz
DPS310_32SAMPLES, ///< 32 Hz
DPS310_64SAMPLES, ///< 64 Hz
DPS310_128SAMPLES, ///< 128 Hz
} dps310_oversample_t;
実際の書き込みの方法は下のようにします。アドレスを書き込んだ後に、値を書いてます。
uint8_t temp = (((uint8_t)DPS310_8HZ)<<4U)|((uint8_t)DPS310_8SAMPLES);
Wire.beginTransmission(0x77);//0x77はデバイスアドレス
Wire.write(0x6); //0x6はレジスタアドレス
Wire.write(temp);//tempは設定値
Wire.endTransmission();
レジスタの値を読み込む方法は下のような感じです。writeメソッドで読み込みたい先頭アドレスを指定して、そのあとrequestFromでデータ長を指定して、readで読みます。
Wire.beginTransmission(0x77);
Wire.write(0x6);
Wire.endTransmission();
Wire.requestFrom(0x77,1,true);
uint8_t temp = Wire.read(); //値をget
温度センサも上と同じように設定した後に、次はMode and Statusレジスタを設定します。モードはリセット直後にアイドル状態(000)となっているため、今回は、気圧、温度を取りにいく前に、(001)か(010)を書き込んで、明示的に値をとりにいきます。
例のごとく、enumでレジスタ値を定義しときます。
typedef enum {
DPS310_IDLE = 0b000, //Stopped/idle
DPS310_ONE_PRESSURE = 0b001, //Take single pressure measurement
DPS310_ONE_TEMPERATURE = 0b010, //Take single temperature measurement
DPS310_CONT_PRESSURE = 0b101, //Continuous pressure measurements
DPS310_CONT_TEMP = 0b110, //Continuous pressure measurements
DPS310_CONT_PRESTEMP = 0b111, //Continuous temp+pressure measurements
} dps310_mode_t;
コードは下のとおりです。
uint8_t temp = 1U; //気圧をとる場合
Wire.beginTransmission(0x77);//0x77はデバイスアドレス
Wire.write(0x8); //0x8はレジスタアドレス
Wire.write(temp);//tempは設定値
Wire.endTransmission();
複数バイトの一気読み
手順1のキャリブレ値は、0x10~0x21のレジスタに格納されてます。
係数用の配列をつくって、一気に読みにいきたいです。writeメソッドで、係数の先頭アドレス0x10を書き込み、requestFromメソッドでデータ長18バイトを指定すればバッファに値がたまり、readメソッドで値を順に読み込めそう。
uint8_t coeffs[18];
Wire.beginTransmission(0x77);
Wire.write(0x10);
Wire.endTransmission();
Wire.requestFrom(0x77,18,true);
for (uint8_t i = 0; i < 18; i++) {
coeffs[i] = Wire.read();
String s = "Coeff[" + String(i) + "] =";
Serial.print(s);
Serial.println(coeffs[i]);
}
下が結果です。とれてそうです。
あとは、これらの値を変換式に代入するだけです。
2の補数の計算
ゲットしたCoeffから、ためしにC20の計算をしてみます。係数はマイナス値も取りうるため、そこらへんの考慮が必要です。
0x1Cのデータをval_msb, 0x1Dのデータをval_lsbとすると、下のように2バイトデータに変換します。
((uint16_t)val_msb<<8U)|((uint16_t)val_lsb); //2バイトデータにする
この値は、マイナスをとりうるらしいので、最上位の符号ビットに1がたつと、値をビット反転して、1足して、マイナス値に換算します。(2の補数+1)
色々やり方がありますが、
val_out = val - (1U<<bits);
でも、
val_out = -(~val + 1);
でもOKです。実際は下のように書きました。
uint8_t bits=16; //データサイズ
int16_t val_out;
uint8_t val_msb = coeffs[12];
uint8_t val_lsb = coeffs[13];
uint16_t val = ((uint16_t)val_msb<<8U)|((uint16_t)val_lsb);
if ((val) & ((uint16_t)1 << (bits - 1))) {
Serial.println("符号ビットが1");
val_out = val - (1U<<bits);
}
else{
val_out = val;
}
Serial.print("val_out = ");
Serial.println(val_out);
こんな感じで係数を換算していきます。
最終的なコード
大体、ポイントとなるところはこれまでの説明で網羅できました。最終的なコードは下のとおり。一部、簡単のため、関数化してたりしますが、基本やってることは同じです。
#include <Arduino.h>
#include <Wire.h>
#define DPS310_I2CADDR_DEFAULT (0x77) ///< Default breakout addres
#define DPS310_PRSB2 0x00 ///< Highest byte of pressure data
#define DPS310_TMPB2 0x03 ///< Highest byte of temperature data
#define DPS310_PRSCFG 0x06 ///< Pressure configuration
#define DPS310_TMPCFG 0x07 ///< Temperature configuration
#define DPS310_MEASCFG 0x08 ///< Sensor configuration
#define DPS310_CFGREG 0x09 ///< Interrupt/FIFO configuration
#define DPS310_COEFF 0x10 ///< Start address of sensor coefficient
#define DPS310_RESET 0x0C ///< Soft reset
#define DPS310_PRODREVID 0x0D ///< Register that contains the part ID
#define DPS310_TMPCOEFSRCE 0x28 ///< Temperature calibration src
#define KP_8TIMES 7864320
#define KT_8TIMES 7864320
/** The measurement rate ranges */
typedef enum {
DPS310_1HZ, ///< 1 Hz
DPS310_2HZ, ///< 2 Hz
DPS310_4HZ, ///< 4 Hz
DPS310_8HZ, ///< 8 Hz
DPS310_16HZ, ///< 16 Hz
DPS310_32HZ, ///< 32 Hz
DPS310_64HZ, ///< 64 Hz
DPS310_128HZ, ///< 128 Hz
} dps310_rate_t;
/** The oversample rate ranges */
typedef enum {
DPS310_1SAMPLE, ///< 1 Hz
DPS310_2SAMPLES, ///< 2 Hz
DPS310_4SAMPLES, ///< 4 Hz
DPS310_8SAMPLES, ///< 8 Hz
DPS310_16SAMPLES, ///< 16 Hz
DPS310_32SAMPLES, ///< 32 Hz
DPS310_64SAMPLES, ///< 64 Hz
DPS310_128SAMPLES, ///< 128 Hz
} dps310_oversample_t;
/** The oversample rate ranges */
typedef enum {
DPS310_IDLE = 0b000, ///< Stopped/idle
DPS310_ONE_PRESSURE = 0b001, ///< Take single pressure measurement
DPS310_ONE_TEMPERATURE = 0b010, ///< Take single temperature measurement
DPS310_CONT_PRESSURE = 0b101, ///< Continuous pressure measurements
DPS310_CONT_TEMP = 0b110, ///< Continuous pressure measurements
DPS310_CONT_PRESTEMP = 0b111, ///< Continuous temp+pressure measurements
} dps310_mode_t;
uint8_t coeffs[18];
uint8_t buf[3];
int16_t c0,c1,c01,c11,c20,c21,c30;
int32_t c00,c10;
int32_t PRS_raw,TMP_raw;
float TMP,PRS;
//2の補数計算(マイナス値を算出する)
int32_t two_complement(uint32_t val,uint8_t bits)
{
int32_t val_out;
if ((val) & ((uint32_t)1 << (bits - 1))) {
val_out = val - (1U<<bits);
}
else{
val_out = val;
}
return val_out;
}
//係数を読み込む関数
void GetCoeff(uint8_t *coeffs)
{
Wire.beginTransmission(DPS310_I2CADDR_DEFAULT);
Wire.write(DPS310_COEFF);
Wire.endTransmission();
Wire.requestFrom(DPS310_I2CADDR_DEFAULT,18,true);
for (uint8_t i = 0; i < 18; i++) {
coeffs[i] = Wire.read();
String s = "Coeff[" + String(i) + "] =";
}
c0 = two_complement(((uint16_t)coeffs[0] << 4) | (((uint16_t)coeffs[1] >> 4) & 0x0F), 12);
c1 = two_complement((((uint16_t)coeffs[1] & 0x0F) << 8) | coeffs[2], 12);
c00 = two_complement(((uint32_t)coeffs[3] << 12) | ((uint32_t)coeffs[4] << 4) |
(((uint32_t)coeffs[5] >> 4) & 0x0F),20);
c10 = two_complement((((uint32_t)coeffs[5] & 0x0F) << 16) | ((uint32_t)coeffs[6] << 8) |
(uint32_t)coeffs[7],20);
c11 = two_complement(((uint16_t)coeffs[10] << 8) | (uint16_t)coeffs[11], 16);
c01 = two_complement(((uint16_t)coeffs[8] << 8) | (uint16_t)coeffs[9], 16);
c20 = two_complement(((uint16_t)coeffs[12] << 8) | (uint16_t)coeffs[13], 16);
c21 = two_complement(((uint16_t)coeffs[14] << 8) | (uint16_t)coeffs[15], 16);
c30 = two_complement(((uint16_t)coeffs[16] << 8) | (uint16_t)coeffs[17], 16);
/*
Serial.println("--- coefficient was loaded ---");
Serial.print("c0 ="); Serial.println(c0);
Serial.print("c1 ="); Serial.println(c1);
Serial.print("c00 ="); Serial.println(c00);
Serial.print("c10 ="); Serial.println(c10);
Serial.print("c01 ="); Serial.println(c01);
Serial.print("c11 ="); Serial.println(c11);
Serial.print("c20 ="); Serial.println(c20);
Serial.print("c21 ="); Serial.println(c21);
Serial.print("c30 ="); Serial.println(c30);
Serial.println("------------------------------");
*/
}
//指定のアドレスに値を書き込む関数
void WriteReg(uint8_t addrs, uint8_t val)
{
Wire.beginTransmission(DPS310_I2CADDR_DEFAULT);
Wire.write(addrs);
Wire.write(val);
Wire.endTransmission();
}
//指定のアドレスから値を読む関数 lengthで読み込むバイト数を指定する
void ReadReg(uint8_t addrs,uint8_t * buf,uint8_t length)
{
Wire.beginTransmission(DPS310_I2CADDR_DEFAULT);
Wire.write(addrs);
Wire.endTransmission();
Wire.requestFrom(DPS310_I2CADDR_DEFAULT,length,true);
for(uint8_t i =0;i<length;i++)
{
buf[i] = Wire.read();
}
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Wire.begin(21, 22);
delay(3000);
uint8_t temp = (((uint8_t)DPS310_8HZ)<<4U)|((uint8_t)DPS310_8SAMPLES);
//Serial.println(temp);
//圧力センササンプリング設定
WriteReg(DPS310_PRSCFG,temp);
//設定値確認
ReadReg(DPS310_PRSCFG,buf,1);
// Serial.print("PRSCFG = 0X");
// Serial.println(buf[0],HEX);
//温度センササンプリング設定
WriteReg(DPS310_TMPCFG,temp);
//設定値確認
ReadReg(DPS310_TMPCFG,buf,1);
// Serial.print("TMPCFG = 0X");
// Serial.println(buf[0],HEX);
//計算に必要な係数をロードする
GetCoeff(coeffs);
}
void loop() {
//温度のセンシング指示
WriteReg(DPS310_MEASCFG,0x02);
//温度データを3バイト分読む
ReadReg(DPS310_TMPB2,buf,3);
TMP_raw = two_complement(((uint32_t)buf[0])<<16|((uint32_t)buf[1])<<8|((uint32_t)buf[2]),24);
float TMP_raw_scale = (float)TMP_raw / KT_8TIMES;
TMP = TMP_raw_scale * c1 + c0 / 2.0;
//結果表示
//Serial.print("TMP[degC] ="); Serial.println(TMP);
//気圧のセンシング指示
WriteReg(DPS310_MEASCFG,0x01);
ReadReg(DPS310_PRSB2,buf,3);
PRS_raw = two_complement(((uint32_t)buf[0])<<16|((uint32_t)buf[1])<<8|((uint32_t)buf[2]),24);
float PRS_raw_scale = (float)PRS_raw / KP_8TIMES;
PRS =(int32_t)c00+PRS_raw_scale * ((int32_t)c10 + PRS_raw_scale * ((int32_t)c20 + PRS_raw_scale * (int32_t)c30)) +
TMP_raw_scale *((int32_t)c01 + PRS_raw_scale * ((int32_t)c11 + PRS_raw_scale * (int32_t)c21));
//結果表示
//Serial.print("PRS[kPa] ="); Serial.println(PRS/1000);
Serial.print("$"); Serial.println(PRS/1000);
//Serial.println();
delay(1000);
}
下が実行結果。
おしまい
説明は、以上です。M5StampPicoの説明なのかDSP310の説明なのか、よく分からなくなってしまいましたが、頑張ってまとめてみました。
そんでわ happy DSP310