ま"’s blog

電子工作の部屋?

温湿度計の自作

100円ショップで温湿度計は売っているし、SwitchBotの温湿度計も試しに買ってみたので、基本的には自作する必要などありません。次のステップでのラズパイとかにつなげてネットワーク上で室温・湿度をモニターできるようにしたいので、まずは技術蓄積が目的です。ATmegaマイコンチップでの消費電力の削減もこのプロジェクトのもう一つテーマです。
実際にはSwitchBotの温湿度計の方が安上がりなので、一時はだいぶ凹みましたが、何かをコントロールしたりMacからSwitchBotへアクセスするにはハブ2が必要とか言うので、自作も悪くはないと思い直して再開しました。

ハードウェアのメインパーツは以下のものを取り上げました。

秋月ばっかりなのは、一つのところで済ませた方が送料をまとめられるからですね。上の他にもバッテリーケースだのトランジスタだのLEDだのダイオードだの抵抗だのコンデンサだの細かい部品もあります。

機能

以下のような機能をつけたいと考えました:

  • 温湿度の測定と表示
  • 電源電圧測定
  • USBコネクタを繋げたら充電。充電中は充電制御
  • USBコネクタを外されたら低消費電力モードになる

回路図

回路図

最初、電源であるバッテリーの3.6V、フル充電で4.3Vで稼働させるつもりで作り始めたのですが、LCDが突然表示されなくなりよくよく説明書読むと「Vddは3.3Vまで」と書いてあり、どうやら壊してしまったようです。
そこから内部動作は3.3V、給電電圧は5Vと2電源構成になってしまったので、読みづらくなってしまいました。申し訳ない。
5Vと3.3Vの1.7Vの差を埋めるために、そこかしこにダイオードショットキーバリアダイオードなどが入ってます(逆流防止の意味もありますが)。ダイオードを通る際の電圧降下で5Vの電圧を下げ、3.3V動作のマイコン出力でLEDやらトランジスタをコントロール可能になってます。
特にLEDのところにつながっているD8(回路図中ではVSENSE)ピンをOUTPUTモードでHIGHにすることで、LEDを点滅可能にしています。
ショットキーバリアダイオード(SD103A)とトランジスタ(2SA1015)の電圧降下でピッタリ充電電圧の4.3Vになっています(ほとんど偶然ですが^^;)。

スケッチ

デバッグ用のコードが残っていて読みにくいです。ごめんなさい。
いろんな人のコードをパクりまくってます。先に謝っておきます。

/*
   From https://qiita.com/kurasho/items/dd908522b3784a9829d7
   http://radiopench.blog96.fc2.com/blog-entry-486.html
*/

#include <avr/power.h>                  // PM関連
#include <avr/sleep.h>                  // スリープライブラリ
#include <avr/wdt.h>                    // ウォッチドッグタイマー ライブラリ
// include the library code:
#include <Wire.h>
#include <ST7032_asukiaaa.h>
#include <SparkFun_Qwiic_Humidity_AHT20.h> //Click here to get the library: http://librarymanager/All#Qwiic_Humidity_AHT20 by SparkFun

// initialize the library
ST7032_asukiaaa lcd;
AHT20 humiditySensor;

#define WDT_8s 9
#define WDT_4s 8
#define WDT_2s 7
#define WDT_1s 6

#define VSENSE_PIN  8
#define CHG_PIN   9

// 基準電圧はチップごとに異なるので、逆算してから設定する
#define REFV  1.08

// #define DEBUG

#ifdef  DEBUG
#define WDT_INTERVAL  2
#else   // DEBUG
#define WDT_INTERVAL  30
#endif  // DEBUG

void setup() {
  // Power Reduction on unused peripheral
  power_all_disable();
  power_adc_enable();
  power_timer0_enable();
  power_twi_enable();
# ifdef DEBUG
  power_usart0_enable();
# endif // DEBUG

  // TIMERのどれかはdelayやI2Cで使いそうな気がする。-> delayはtimer0でした。
  // I2Cもtimer0を必要とするようだ。

  // 使用するポートはINPUTモードに
# ifdef DEBUG
  Serial.begin(9600);
# endif // DEBUG
  pinMode(VSENSE_PIN, INPUT);   // D8:外部電源検知用
  pinMode(CHG_PIN, OUTPUT);     // D9:充電制御用
  digitalWrite(CHG_PIN, HIGH);  // Default HIGH in order to power save.

  // set up the LCD:
  lcd.begin(16, 2);
  lcd.setContrast(35);
  // Print a message to the LCD.
  lcd.print("hello!");

  humiditySensor.begin();
}

int count = 0;
int chgTime = 0;

void loop() {
  // put your main code here, to run repeatedly:
  float vcc;
  float temperature;
  float humidity;
  int   wdtInterval = WDT_INTERVAL;

  // 通常モードに復帰
  vcc = cpuVcc();

  lcd.begin(16, 2);
  lcd.setContrast(40);

  while (humiditySensor.available() != true)
    delay(1);
  temperature = humiditySensor.getTemperature();
  humidity = humiditySensor.getHumidity();

# ifdef DEBUG
  Serial.print("Vcc = ");
  Serial.println(vcc, 2);
  //Print the results
  Serial.print("Temperature: ");
  Serial.print(temperature, 2);
  Serial.print(" C\t");
  Serial.print("Humidity: ");
  Serial.print(humidity, 2);
  Serial.println("% RH");
# endif // DEBUG

  // set the cursor to column 0, line 0
  lcd.setCursor(0, 0);
  lcd.print("Tmp:");
  lcd.print(temperature, 1);
  lcd.print("C\337");

  lcd.setCursor(0, 1);
  lcd.print("Hum:");
  lcd.print(humidity, 1);
  lcd.print("%");

  lcd.setCursor(11, 0);
  lcd.print("Vcc:");
  lcd.setCursor(11, 1);
  lcd.print(vcc, 2);
  lcd.print("V");

  pinMode(VSENSE_PIN, INPUT);   // WDTによってOUTPUTモードにリセットされる?
  if (digitalRead(VSENSE_PIN) == HIGH) {
    // Shift to charge mode
#   ifdef DEBUG
    Serial.println("DC on");  // for debug
#   endif // DEBUG

    wdtInterval = 2;    // Shortly loop when power supplyed
    if (chgTime > 27000) { // 15h
      analogWrite(CHG_PIN, (256 - 32));  // Trickle Charge
      delay(40); // Shortly blink LED
      pinMode(VSENSE_PIN, OUTPUT);
      digitalWrite(VSENSE_PIN, HIGH);   // 電位差が1.5v以下になればLEDは点灯しない
    } else {
      chgTime++;
      pinMode(CHG_PIN, OUTPUT);
      digitalWrite(CHG_PIN, LOW);   // Charge on
#    ifdef DEBUG
      if (chgTime % 2) {  // 交互
        pinMode(VSENSE_PIN, OUTPUT);
        digitalWrite(VSENSE_PIN, HIGH);   // 電位差が1.5v以下になればLEDは点灯しない
        digitalWrite(CHG_PIN, HIGH);
      } else {
        pinMode(VSENSE_PIN, INPUT);
        digitalWrite(CHG_PIN, LOW);
      }
#    else
      pinMode(VSENSE_PIN, INPUT);
#    endif   
    }
  } else {
#   ifdef DEBUG
    Serial.println("DC off");  // for debug
    if (digitalRead(CHG_PIN) != LOW)  // 交互 for debug
      digitalWrite(CHG_PIN, LOW);
    else
#   endif // DEBUG

      digitalWrite(CHG_PIN, HIGH);   // Default HIGH in order to power save.
    pinMode(VSENSE_PIN, INPUT);
    chgTime = 0;
  }

  // 低電流でスリープ
  delayWDT(wdtInterval);                         // 引数でスリープ時間指定
}

void setupWDT( uint8_t delay )
{
  // ウォッチドッグタイマのタイムアウト設定
  if ( delay > WDT_8s ) delay = WDT_8s;
  uint8_t reg_data = delay & 7;
  if ( delay & 0x8 )
  {
    reg_data |= 0x20;
  }

  MCUSR &= ~(1 << WDRF);
  WDTCSR |= (1 << WDCE) | (1 << WDE); // 設定変更許可
  // WDTCSR = reg_data | (1 << WDE);  // タイムアウト設定
  // これは誤り。これではタイムアウトでリセットもかけられる設定になる。
  // 正しくは以下。割り込みだけ有効になる。
  WDTCSR = reg_data;  // タイムアウト設定
  WDTCSR |= (1 << WDIE); // 割り込み許可
}

/*
   sleep t seconds
*/
void delayWDT(unsigned long t)
{
  // byte  prevPRR;
# ifdef DEBUG
  Serial.flush();                       // シリアルバッファが空になるまで待つ
# endif // DEBUG

  // ADCを停止(消費電流 147→27μA)
  // ADCSRA &= ~(1 << ADEN);

  power_adc_disable();

  while (t >= 8) {
    sleepWDT(WDT_8s);
    t -= 8;
  }
  while (t >= 4) {
    sleepWDT(WDT_4s);
    t -= 4;
  }
  while (t >= 2) {
    sleepWDT(WDT_2s);
    t -= 2;
  }
  while (t > 0) {
    sleepWDT(WDT_1s);
    t--;
  }

  // 元に戻す
  power_adc_enable();

  // ADCSRA |= (1 << ADEN);                 // ADCの電源をON
}

void  sleepWDT(uint8_t mode)
{
  setupWDT(mode);

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // パワーダウンモード指定
  sleep_enable();
  // sei(); // 割込みを許可する。
  // sleep_bod_disable();
  sleep_cpu(); // ここでCPU停止
  // sleep_mode();
  // cli();
  sleep_disable();                      // WDTがタイムアップでここから動作再開
}

ISR(WDT_vect) {                         // WDTがタイムアップした時に実行される処理
  //  wdt_cycle++;                      // 必要ならコメントアウトを外す
}


float cpuVcc() {                     // 電源電圧(AVCC)測定関数
  long sum = 0;
  adcSetup(0x4E);                    // Vref=AVcc, input=internal1.1V
  for (int n = 0; n < 10; n++) {
    sum = sum + adc();               // adcの値を読んで積分
  }
  return (REFV * 10240.0) / sum;      // 電圧を計算して戻り値にする. チップごとに補正の必要あり
}

void adcSetup(byte data) {           // ADコンバーターの設定
  ADMUX = data;                      // ADC Multiplexer Select Reg.
  ADCSRA |= ( 1 << ADEN);            // ADC イネーブル
  ADCSRA |= 0x07;                    // AD変換クロック CK/128
  delay(10);                         // 安定するまで待つ
}

unsigned int adc() {                 // ADCの値を読む
  unsigned int dL, dH;
  ADCSRA |= ( 1 << ADSC);            // AD変換開始
  while (ADCSRA & ( 1 << ADSC) ) {   // 変換完了待ち
  }
  dL = ADCL;                         // LSB側読み出し
  dH = ADCH;                         // MSB側
  return dL | (dH << 8);             // 10ビットに合成した値を返す
}

低消費電力化のためにパワーダウンモードというCPUを止めてWatch Dog Timerで起きるという大技を使っているため、loop関数の先頭でいくつかの項目の初期化が必要になってます。
動作中の消費電力も最小限に抑えるために、一旦全てをdisableしてから使うものだけenableしています。
基準電圧はマイコンの説明書には1.1Vだと書かれているのですが、チップごとに異なるので、実際にADCで測定したのち逆算してからREFVという定数に設定してください。

ブレッドボードでの試作

今回はここまでです。本番の制作記事はもうしばらくお待ちください。_O_