CO2モニタ2号機の作成(4)

正月休みに作ったCO2モニタですが、早くも壊れてしまいました。

電源を入れっぱなし(というか切れないのですが)で放置していたところ、Seeeduino Xiao内部のXC9306が壊れてしまったようです。5Vを供給しても3.3V側に電圧が出てこなくなりました。
ですので、電源を供給してもLEDが点灯しません。

残念ですが、この構成は無理があるようなので、解体することにしました。
(Nokia液晶も実はバックライトが4個中2個点灯しない・・・)

他にSensirionのSCD30とかもあるのですが、どうしましょうかね。SCD30も同じ構成でSeeeduino Xiaoで動かそうかと思って違う液晶モジュール用にコードを書いている途中だったのですが、バッテリ切れで壊れてしまうのではこの構成は取れません。

できれば自分でチップ集めてボード作りたいところで、いまならRaspberry Pi Picoに搭載されるRP2040でボードを作りたいところですが、このチップは単体で売られるかなぁ。売られることを期待したいのですが・・・。

ATTINY2313で商用電源周波数監視

ATmega328で商用電源周波数の監視ができるようになりましたが、グラフを見てないと変動に気づきません。そこで、スタンドアローンで動くものを余っているATTINY2313で作ってみました。

基本的な構成はATmega328の時と同じです。100均のニッケル水素充電器のマイナス端子から内部のトランスの2次側電圧を引き出して、半波整流してLP2950L-3.3でATTINY2313の3.3V電源を生成します。一方でフォトカプラで商用電源の周期に同期した信号を生成して、ATTINY2313のTIMER1のICP信号に入力します。ATTINY2313の動作クロックは8MHzのクリスタルとして、適当に作っても最悪100ppm程度の精度は得られるようにします。スタンドアローンで動作するように、OC0B信号を圧電ブザーに接続して、TIMER0でブザーを鳴らせるようにします。

完成したものはこんな感じになります。

基板上のLEDはUARTのTXに接続してあるので、そこにトランジスタをつけたフォトダイオードをかざしてやると、非接触でシリアル通信がができます。

これをArduinoのシリアルプロッタに食わせると以下のような感じです。周波数は実際の周波数を1000倍したものです。

シリアルライブラリなどのArduinoのライブラリは使えないのでその部分は簡単なものを作っていますが、ソフトウェアはATmega328の時と大きくは変わりません。(下記の中で&が化けています・・・)

#include <avr/io.h>   // /usr/lib/avr/include/
#include <util/delay.h>
#include <util/atomic.h>
#include <string.h>

// FUSEは外部クリスタル8MHz、システムクロックを分周なしに設定(E:FF, H:DF, L:EF)

/////////////////////////////////////////////////
// 端子定義
#define BUZ 5	 // PORT D5 に圧電ブザー接続
#define ICPPIN 6 // PORT D6 = ICP
#define UARTTX 1 // PORT D1 = TX

/////////////////////////////////////////////////
// タイマ0関連
void Buzzer(uint8_t period){
    if(period>0){
	    // 高速PWM設定によるブザー駆動
	    TCCR0A = 0x23;     // COM0B[1-0]=10,WGM0[1-0]=11  高速PWM動作
	    TCCR0B = 0x0C;     // WGM02=1,CS[2-0]=4  分周=256分周=8MHz/256=31.25kHz
	    OCR0A = period;    // FREQ 31.25kHz / 31 ≒ 1008Hz / 63 ≒ 496Hz
	    OCR0B = period>>1; // DUTYを設定
	} else {
	    TCCR0A = 0x00;  // 出力停止
	}
}

/////////////////////////////////////////////////
// タイマ1関連
// 周期測定ののための変数
volatile uint16_t NumV;
volatile uint32_t SumV;
volatile uint16_t CapV;
volatile uint16_t PreCap,NowCap;

// 1秒間の最大最小平均を格納するための変数
uint16_t tNumV;
uint32_t tAveV;
uint32_t tSumV;

// 割り込みハンドラ
ISR(TIMER1_CAPT_vect){
  TIFR |= (1<<ICF1); 
  NowCap = ICR1;
  CapV = NowCap - PreCap;       // 差分を求めて周期を算出
  PreCap = NowCap;
  SumV += CapV;
  NumV++;
}

// タイマ1設定
void TIMER1_Init(void){
    // TCC1でICP1信号(PB0端子=IO8)の周期を計測する
    TCCR1A = 0; //initialize Timer1
    TCCR1B = 0;
    TCNT1 = 0;

    TCCR1B =  0x02;   // 8MHzを8分周
    TIMSK  |= (1 << ICIE1); // キャプチャ割り込み許可
}

uint32_t Period(void){
    // 割り込みで更新している変数をコピー&初期化する
    cli(); //割り込み停止
    tSumV = SumV; tNumV = NumV;
    SumV = 0; NumV = 0;
    sei();   //割り込み再開
    tAveV = tSumV / tNumV;
    return(tAveV);
}

/////////////////////////////////////////////////
// UART関連
#define FOSC 8000000
#define BAUD 38400
#define MYUBRR FOSC/16/BAUD-1

uint8_t txbuf[16];
uint8_t txp=0,txn=0;

ISR(USART_UDRE_vect){
  if(txp<txn){
    while ( !(UCSRA &amp; (1<<UDRE)) );
    UDR = txbuf[txp];
    txp++;
  } else {
    UCSRB &amp;= ~_BV(UDRIE);
  }
}

void USART_Init(unsigned int baud){
    UBRRH = (unsigned char)(baud>>8);
    UBRRL = (unsigned char)baud;
    UCSRC = (1<<USBS)|(3<<UCSZ0);
    txp = 0;
    UCSRB = (1<<RXEN)|(1<<TXEN)|(1<<UDRIE);
}

void USART_txt(uint8_t *txt,uint8_t len){
    while(txp<txn);
    cli();
    memcpy(txbuf,txt,len);
    txp=0;
    txn=len;
    UCSRB |= _BV(UDRIE);
    sei();
}

void USART_num(uint16_t num){
    uint16_t d=10000;
    uint16_t n=num;
    uint8_t  c,p=0,b[8];
    while(d>0){
        c=n/d; n=n%d;
        d=d/10;
        b[p++]=c+0x30;
    }
    b[p++]='\r'; b[p++]='\n';
    USART_txt(b,p);
}

void USART_znum(uint16_t num){
    uint8_t s=1;    // ゼロサプレス中
    uint16_t d=10000;
    uint16_t n=num;
    uint8_t  c,p=0,b[8];
    while(d>0){
        c=n/d; n=n%d;
        d=d/10;
        if(d==0) s=0;
        if(s==0 || c!=0){
            b[p++]=c+0x30;
            s=0;
        }
    }
    b[p++]='\r'; b[p++]='\n';
    USART_txt(b,p);
}

/////////////////////////////////////////////////
// メイン
int main(void)
{
    uint32_t f,p,d;

    PORTB  = 0xFF; // 入力設定時のプルアップ有効
    PORTD  = 0xFF; // 入力設定時のプルアップ有効
    DDRD   = _BV(BUZ);     // PORT D5を出力に
    DDRD  &amp;= ~_BV(ICPPIN); // ICPピンを入力に
    PORTD &amp;= ~_BV(ICPPIN); // ICPピンのプルアップ無効化
    PORTD  = _BV(UARTTX);  // PORT D1を出力に

    TIMER1_Init();

    USART_Init(MYUBRR);

    for(;;){
        p = Period();
        f = 1000000000UL/p;  // 周波数の1000倍を求める
        if(f > 50000){
            d = f - 50000;
        } else {
            d = 50000 - f;
        }
        if(d > 200){      // 0.20Hz以上ずれた場合
            Buzzer(31);  // 高速PWM設定によるブザー駆動 31=1kHz
        }else if(d > 150){ // 0.15Hz以上ずれた場合
            Buzzer( 78);  // 高速PWM設定によるブザー駆動 78=400Hz
        }else if(d > 100){ // 0.10Hz以上ずれた場合
            Buzzer(250);  // 高速PWM設定によるブザー駆動 250=125Hz
        }else {
            Buzzer(0);
        }
        USART_znum(f);
        _delay_ms(1000);
    }
}

いまさらATTINY2313

ジャンク箱を漁っていたら、ATTINY2313がたくさん出てきたので消費することを考えてみました。開発環境はもちろんLinux Mint 20です。

開発環境をインストール

単純に

$ sudo apt-get install gcc-avr binutil-avr avr-libc avrdude

でいけます。あと、udevのルールに

# avrispmkII programmer rules (https://www.microchip.com/DevelopmentTools/ProductDetails/PartNO/ATAVRISP2)
SUBSYSTEMS=="usb", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2104", GROUP="users", MODE="0666"

を追加しておきます(参考:https://aur.archlinux.org/packages/avrisp-udev/

AVRISP mkIIの接続

AVRISP mkIIをつないで、Fuseビットを読み出してみます。

$ avrdude -P usb -c avrisp2 -p attiny2313
avrdude: AVR device initialized and ready to accept instructions
Reading | ################################################## | 100% 0.00s
avrdude: Device signature = 0x1e910a (probably t2313)
avrdude: safemode: Fuses OK (E:FF, H:DF, L:64)
avrdude done.  Thank you.

ヒューズビットの初期値は、拡張=FFH、上位=DFH、下位=64Hで、自己プログラミング不可、シリアルプログラミング許可、WDT無効、BOD禁止、PA2端子機能=RESET、システムクロックは8分周、システムクロック外部出力禁止、SUT=2(電源電圧の立ち上がりが遅い場合に対応)、CKSEL=4(内蔵OSC=8MHz)、ということになります。外部クリスタルを使う場合はCKSELを書き換える必要があります。ヒューズビットを書き換える場合には以下のようにします。

$ avrdude -p attiny2313 -c avrispmkII -B 125kHz -U lfuse:w:0x64:m -U hfuse:w:0xDF:m

サンプルソース

以下のソースで試してみます。

#include <avr/io.h>
#include <util/delay.h>

#define BBLED 3

int main(void)
{
    DDRB  = _BV(BBLED);

    for(;;){
        PORTB ^= _BV(BBLED);
        _delay_ms(250);
    }
}

コンパイル

以下でコンパイル、ディスアセンブルできました。

$ avr-gcc -g -O2 -mmcu=attiny2313 -DF_CPU=1000000UL -c -o led.o led.c
$ avr-gcc -g -O2 -mmcu=attiny2313 led.o -o led
$ avr-objcopy -j .text -j .data -O ihex led led.ihex
$ avr-objdump -d led

led:     ファイル形式 elf32-avr


セクション .text の逆アセンブル:

00000000 <__vectors>:
   0:	12 c0       	rjmp	.+36     	; 0x26 <__ctors_end>
   2:	17 c0       	rjmp	.+46     	; 0x32 <__bad_interrupt>
   4:	16 c0       	rjmp	.+44     	; 0x32 <__bad_interrupt>
   6:	15 c0       	rjmp	.+42     	; 0x32 <__bad_interrupt>
   8:	14 c0       	rjmp	.+40     	; 0x32 <__bad_interrupt>
   a:	13 c0       	rjmp	.+38     	; 0x32 <__bad_interrupt>
   c:	12 c0       	rjmp	.+36     	; 0x32 <__bad_interrupt>
   e:	11 c0       	rjmp	.+34     	; 0x32 <__bad_interrupt>
  10:	10 c0       	rjmp	.+32     	; 0x32 <__bad_interrupt>
  12:	0f c0       	rjmp	.+30     	; 0x32 <__bad_interrupt>
  14:	0e c0       	rjmp	.+28     	; 0x32 <__bad_interrupt>
  16:	0d c0       	rjmp	.+26     	; 0x32 <__bad_interrupt>
  18:	0c c0       	rjmp	.+24     	; 0x32 <__bad_interrupt>
  1a:	0b c0       	rjmp	.+22     	; 0x32 <__bad_interrupt>
  1c:	0a c0       	rjmp	.+20     	; 0x32 <__bad_interrupt>
  1e:	09 c0       	rjmp	.+18     	; 0x32 <__bad_interrupt>
  20:	08 c0       	rjmp	.+16     	; 0x32 <__bad_interrupt>
  22:	07 c0       	rjmp	.+14     	; 0x32 <__bad_interrupt>
  24:	06 c0       	rjmp	.+12     	; 0x32 <__bad_interrupt>

00000026 <__ctors_end>:
  26:	11 24       	eor	r1, r1
  28:	1f be       	out	0x3f, r1	; 63
  2a:	cf ed       	ldi	r28, 0xDF	; 223
  2c:	cd bf       	out	0x3d, r28	; 61
  2e:	02 d0       	rcall	.+4      	; 0x34 <main>
  30:	0e c0       	rjmp	.+28     	; 0x4e <_exit>

00000032 <__bad_interrupt>:
  32:	e6 cf       	rjmp	.-52     	; 0x0 <__vectors>

00000034 <main>:
  34:	88 e0       	ldi	r24, 0x08	; 8
  36:	87 bb       	out	0x17, r24	; 23
  38:	98 e0       	ldi	r25, 0x08	; 8
  3a:	88 b3       	in	r24, 0x18	; 24
  3c:	89 27       	eor	r24, r25
  3e:	88 bb       	out	0x18, r24	; 24
  40:	e3 e2       	ldi	r30, 0x23	; 35
  42:	f4 ef       	ldi	r31, 0xF4	; 244
  44:	31 97       	sbiw	r30, 0x01	; 1
  46:	f1 f7       	brne	.-4      	; 0x44 <__SREG__+0x5>
  48:	00 c0       	rjmp	.+0      	; 0x4a <__SREG__+0xb>
  4a:	00 00       	nop
  4c:	f6 cf       	rjmp	.-20     	; 0x3a <main+0x6>

0000004e <_exit>:
  4e:	f8 94       	cli

00000050 <__stop_program>:
  50:	ff cf       	rjmp	.-2      	; 0x50 <__stop_program>
$ 
$ avrdude -P usb -c avrisp2 -p attiny2313 -U flash:w:led

最後の書き込みの部分は、HEXファイルを指定せず、バイナリを直接指定しても大丈夫です。

圧電ブザーを鳴らしてみる

LED点滅に加えて、圧電ブザーを鳴動させてみます。圧電ブザーはタイマ0で駆動し、PORT D5=OCR0B端子に接続、高速PWM設定でタイマ0を動作させて圧電ブザーへのデューティーも制御してみます。(デューティーを可変させるのは圧電ブザーではあまり意味がなかった・・・)

ソースコードは以下の通りです。

#include <avr/io.h>   // /usr/lib/avr/include/
#include <util/delay.h>

// FUSEは初期値のまま(E:FF, H:DF, L:64)
// クロックは内蔵OSCで8MHz、システムクロック分周=8

#define BBLED 3 // PORT B3 にLED接続
#define BUZ 5   // PORT D5 に圧電ブザー接続

int main(void)
{
    DDRB  = _BV(BBLED);
    DDRD  = _BV(BUZ);

    for(;;){
        PORTB ^= _BV(BBLED);
        _delay_ms(250);

        if(PORTB &amp; _BV(BBLED)){
            // 高速PWM設定によるブザー駆動
            TCCR0A = 0x23; // COM0B[1-0]=10,WGM0[1-0]=11  高速PWM動作
            TCCR0B = 0x0A; // WGM02=1,CS[2-0]=2  分周=8分周=1MHz/64=125kHz
            OCR0A = 125; // FREQ 125kHz / 125 = 1000Hz / 250 = 500Hz
            OCR0B = 62;  // DUTYを設定=VOLUME / COM0B=10
            // OC0B output
        } else {
            TCCR0A = 0x00; // 出力停止
        }
    }
}

Makefileを用意する

ArchiLinuxのページにAVR用のMakefileのサンプルがあったので、それを参考にMakefileを用意してみました。

CC = avr-gcc
OBJCOPY = avr-objcopy
SIZE = avr-size
NM = avr-nm
AVRDUDE = avrdude
REMOVE = rm -f

MCU = attiny2313
F_CPU = 1000000

LFUSE = 0x64
HFUSE = 0xDF

TARGET = firmware
SRC = main.c
OBJ = $(SRC:.c=.o)
LST = $(SRC:.c=.lst)

FORMAT = ihex

OPTLEVEL = s

CDEFS = 

CFLAGS = -DF_CPU=$(F_CPU)UL
CFLAGS += $(CDEFS)
CFLAGS += -O$(OPTLEVEL)
CFLAGS += -mmcu=$(MCU)
CFLAGS += -std=gnu99
CFLAGS += -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums
CFLAGS += -ffunction-sections -fdata-sections
CFLAGS += -Wall -Wstrict-prototypes
CFLAGS += -Wa,-adhlns=$(<:.c=.lst)

LDFLAGS = -Wl,--gc-sections
LDFLAGS += -Wl,--print-gc-sections

AVRDUDE_MCU = attiny2313
AVRDUDE_PROGRAMMER = avrisp2
AVRDUDE_SPEED = -B 125kHz

AVRDUDE_FLAGS = -p $(AVRDUDE_MCU)
AVRDUDE_FLAGS += -P usb
AVRDUDE_FLAGS += -c $(AVRDUDE_PROGRAMMER)
AVRDUDE_FLAGS += $(AVRDUDE_SPEED)

MSG_LINKING = Linking:
MSG_COMPILING = Compiling:
MSG_FLASH = Preparing HEX file:

all: gccversion $(TARGET).elf $(TARGET).hex size

.SECONDARY: $(TARGET).elf
.PRECIOUS: $(OBJ)

%.hex: %.elf
	@echo
	@echo $(MSG_FLASH) $@
	$(OBJCOPY) -O $(FORMAT) -j .text -j .data $< $@

%.elf: $(OBJ)
	@echo
	@echo $(MSG_LINKING) $@
	$(CC) -mmcu=$(MCU) $(LDFLAGS) $^ --output $(@F)

%.o : %.c
	@echo $(MSG_COMPILING) $<
	$(CC) $(CFLAGS) -c $< -o $(@F)

gccversion:
	@$(CC) --version

size: $(TARGET).elf
	@echo
	$(SIZE) -C --mcu=$(AVRDUDE_MCU) $(TARGET).elf

analyze: $(TARGET).elf
	$(NM) -S --size-sort -t decimal $(TARGET).elf

isp: $(TARGET).hex
	$(AVRDUDE) $(AVRDUDE_FLAGS) -U flash:w:$(TARGET).hex

fuses:
	$(AVRDUDE) $(AVRDUDE_FLAGS) -U lfuse:w:$(LFUSE):m -U hfuse:w:$(HFUSE):m

release: fuses isp

clean:
	$(REMOVE) $(TARGET).hex $(TARGET).elf $(OBJ) $(LST) *~

無事に、

$ make isp fuses

でビルドと書き込みができました。fusesは1回だけ指定すればOKです。

電源周波数の変動を測ってみた

最近の寒波で電力供給の危機が叫ばれています。原子力発電所が震災以来(ほぼ?)停止していて、CO2の対策として石炭火力や石油での火力もだいぶ止まっているような話を聞きます。そこへ来て、大雪によってソーラーパネルに雪が積もって発電できないのですから当たり前といえば当たり前です。

電力供給が厳しくなると発電所から見た負荷が重くなるので、電源周波数の変動として見えてくるはずです。そこで、電源周波数の変動を見てみよう、ということになります。

電源周波数の取り出し

ちゃんと見ようと思うと、どうしても商用電源へのアクセスが必要で感電防止とかを考えると大がかりになりがちですが、今回は100円ショップで売っているニッケル水素電池の充電器を使うことにしました。100円ショップで入手したものは、以前分解したときに(電池を入れない状態では)電池のマイナス端子に内部のトランスの端子がそのまま出ていることに気づきました。そこで、ここから商用電源の信号を取ることにします。(注意: 感電の恐れが大きい部分に触れることなく取り出せますが、この充電器の本来の使い方とは異なりますので、まねする場合には十分注意して行うとともに、自己責任で行ってください。似た形状で回路構成が違うものもあるかもしれませんし。)

このようにオシロスコープのプローブを2つの電池のマイナス端子に接続して波形観測すると、Peak-to-Peakで12Vくらいあることがわかります。ここにフォトカプラを入れて信号を取り出すことを考えます。

手持ちのフォトカプラPC817のデータシートを見ると、以下の特性を持っています。

フォトカプラの内蔵LEDにはIFは5mAも流してやれば良さそうです。VF=1.2V程度なので、ピーク時の6Vの際に10mA程度をターゲットに470Ωをつけてやることにします。抵抗には10mA程度流れるときに5Vの電圧がかかるので、50mWの消費電力・・・と思ったけど直流ではないので平均はもっと少ないですが、1/6Wのものを使うことにしました。あとはフォトカプラの内蔵LEDと逆向きに適当なLEDをつけておきます。これは何でもよく、AC電圧が逆相の際にフォトカプラの内蔵LEDに逆電圧がかかるのを防止するためです。
フォトカプラのフォトトランジスタ側の負荷抵抗は50Hzや60Hzでは周波数特性もへったくれもないので、IC=0.5mAも流れていれば十分ですから4.7kΩとしました。(よく考えたら、この抵抗は無しでIO8の内蔵プルアップを有効にするだけで十分かもしれない)

回路はこんな感じ。左のAC信号源の部分がニッケル水素充電器の2つのマイナス端子。真ん中のダイオードはLEDでフォトカプラの保護用。

ブレッドボード上に組んで波形を観測してみました。

右側のボードは秋月のAE-ATmega328ボードがボードのみで売られていた時代に組んだものです。ATmega328を載せてブートローダを書き込んだのだと思います。クリスタルは16MHzのものが載せてあって、Arduino Pro互換として動作していると思います。フォトカプラの出力はIO8へ接続しています。ATmega328のTCC1のカウント値をキャプチャする機能の入力信号ICP1として使用するためです。

上が充電器の2つの端子の電圧で、下がArduino互換ボードへの信号です。狙ったとおりの信号が得られています。

周期の計測

これをATmega328で周波数を計測します。TCC1の16ビットタイマをCPUのクロックを8分周したもの(つまり2MHz=0.5uS)でフリーランさせて、インプットキャプチャ機能でICP1信号(IO8)の下がりエッジでその瞬間の値をICR1レジスタに取得します。取得したら割り込みでただちにICR1レジスタの値を読み出して保存します。実際には1秒周期で平均周波数を求めてホストに送信するので、割り込み内ではlongの変数に積算するとともに、積算回数をカウントします。また、よりデータを取る場合にはその1秒間での最短周期や最長周期も取得しておきます。

メインのループ側では1秒毎に積算値から平均の周期もしくは周波数を求めてシリアルでホストに送信します。それをArduino IDEのシリアルプロッタの機能でグラフ化するとこんな感じになります。

縦軸は周波数で、単位は1/1000Hzです。50000が50Hzぴったりということになります。このグラフの例では縦軸は49.92Hzから50.16Hzまでが表示されていて、電源の周波数は49.94Hzくらいから50.1Hzくらいまで変動しているということがわかります。右側はサンプル数ですが、データ送信は1秒間に1回なので、過去500秒間の電源周波数変動が見えます。

これをブレッドボードではなくて、ユニバーサル基板で組んでみました。

Arduino用のユニバーサル基板もあったのですが、回路規模が小さいのでより小さなユニバーサル基板で組んでみました。この範囲には5Vがないので、IO12をH出力してフォトカプラの内蔵フォトトランジスタの負荷抵抗の電源端子として使用することにしました。(注:実際には、IO8の内蔵プルアップを有効にするだけでも十分そうです。頑張れば更に小さいユニバーサル基板でも入ったかも。)

さらにソフトを修正して、1秒間の間の最短周期と最大周期も送信するようにしたところ、以下のような感じになりました。

理由はきちんと確認していませんが、1秒間の中での周波数変動が大きい時間と小さい時間があるようです。おそらくはインバータ機器などが動作している/していない期間なのでしょう。

ソフトウェア

平均のみをホストに送信する場合

#define TCCCLK 2000000    // CPUクロックを8分周した値。
                          // Pro Miniなど8MHzで動作している製品の場合は 1000000 にする
#define ICP1 8  // ICP1の端子を定義
#define PWR  12 // フォトカプラの負荷抵抗に電源を供給するための端子

// 周期測定ののための変数
volatile uint16_t NumV;
volatile uint32_t SumV;
volatile uint16_t CapV;
volatile uint16_t PreCap,NowCap;

// 1秒間の最大最小平均を格納するための変数
uint16_t tNumV;
float tAveV;
uint32_t tSumV;

unsigned long current_time;

// 割り込みハンドラ
ISR(TIMER1_CAPT_vect) {
  TIFR1 |= (1<<ICF1); 
  NowCap = ICR1;
  CapV = NowCap - PreCap;       // 差分を求めて周期を算出
  PreCap = NowCap;
  SumV += CapV;
  NumV++;
}

void setup()
{
  Serial.begin(38400);
  pinMode(LED_BUILTIN, OUTPUT);
  
  // TCC1でICP1信号(PB0端子=IO8)の周期を計測する
  TCCR1A = 0; //initialize Timer1
  TCCR1B = 0;
  TCNT1 = 0;
  
  // 4.7kΩなしで端子の内蔵プルアップで済ます場合
  //  pinMode(ICP1, INPUT_PULLUP);  //ICP1の端子をプルアップなしの入力に設定する
  //  pinMode(PWR,  INPUT);

  // 4.7kΩのプルアップありでこれをIO12から供給する場合
  pinMode(ICP1, INPUT);  //ICP1の端子をプルアップなしの入力に設定する
  pinMode(PWR,  OUTPUT);
  digitalWrite(PWR, HIGH);

  TCCR1B =  0x02;   // 16MHzを8分周
  TIMSK1 |= (1 << ICIE1); // キャプチャ割り込み許可
  current_time = millis();
}

#define FREQ  // 計測を周波数で行う場合

void loop()
{
  static uint8_t skp = 3;   // 最初の何回かはデータを捨てる
  
  if (millis() - current_time >= 1000)
  {
    // 割り込みで更新している変数をコピー&初期化する
    noInterrupts(); //割り込み停止
    tSumV = SumV; tNumV = NumV;
    SumV = 0; NumV = 0;
    interrupts();   //割り込み再開
    tAveV = (float)tSumV / (float)tNumV;
    
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    current_time = millis();

    // PCに送信
    if(skp == 0){
      #ifdef FREQ
      Serial.println((double)TCCCLK/(double)tAveV*1000.);
      #else
      Serial.println(tAveV/2);  // これで単位がusになる
      #endif
    } else {
      skp--;
    }
  }
}

1秒間の中の最大/最小も送信する場合

#define TCCCLK 2000000    // CPUクロックを8分周した値。
                          // Pro Miniなど8MHzで動作している製品の場合は 1000000 にする
#define ICP1 8  // ICP1の端子を定義
#define PWR  12 // フォトカプラの負荷抵抗に電源を供給するための端子

// 周期測定ののための変数
volatile uint16_t MaxV,MinV,NumV;
volatile uint32_t SumV;
volatile uint16_t CapV;
volatile uint16_t PreCap,NowCap;

// 1秒間の最大最小平均を格納するための変数
uint16_t tMaxV,tMinV,tNumV;
float tAveV;
uint32_t tSumV;

unsigned long current_time;

// 割り込みハンドラ
ISR(TIMER1_CAPT_vect) {
  TIFR1 |= (1<<ICF1); 
  NowCap = ICR1;
  CapV = NowCap - PreCap;       // 差分を求めて周期を算出
  PreCap = NowCap;
  if(CapV > MaxV) MaxV = CapV;
  if(CapV < MinV) MinV = CapV;
  SumV += CapV;
  NumV++;
}

void setup()
{
  Serial.begin(38400);
  pinMode(LED_BUILTIN, OUTPUT);
  
  // TCC1でICP1信号(PB0端子=IO8)の周期を計測する
  TCCR1A = 0; //initialize Timer1
  TCCR1B = 0;
  TCNT1 = 0;
  
  // 4.7kΩなしで端子の内蔵プルアップで済ます場合
  //  pinMode(ICP1, INPUT_PULLUP);  //ICP1の端子をプルアップなしの入力に設定する
  //  pinMode(PWR,  INPUT);

  // 4.7kΩのプルアップありでこれをIO12から供給する場合
  pinMode(ICP1, INPUT);  //ICP1の端子をプルアップなしの入力に設定する
  pinMode(PWR,  OUTPUT);
  digitalWrite(PWR, HIGH);

  TCCR1B =  0x02;   // 16MHzを8分周
  TIMSK1 |= (1 << ICIE1); // キャプチャ割り込み許可
  current_time = millis();
}

#define FREQ  // 計測を周波数で行う場合

void loop()
{
  static uint8_t skp = 3;   // 最初の何回かはデータを捨てる
  
  if (millis() - current_time >= 1000)
  {
    // 割り込みで更新している変数をコピー&初期化する
    noInterrupts(); //割り込み停止
    tMaxV = MaxV; tMinV = MinV; tSumV = SumV; tNumV = NumV;
    MaxV = 0; MinV = 65535; SumV = 0; NumV = 0;
    interrupts();   //割り込み再開
    tAveV = (float)tSumV / (float)tNumV;
    
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    current_time = millis();

    // PCに送信
    if(skp == 0){
      #ifdef FREQ
      Serial.print((double)TCCCLK/(double)tMaxV*1000.);
      Serial.print(",");
      Serial.print((double)TCCCLK/(double)tMinV*1000.);
      Serial.print(",");
      Serial.println((double)TCCCLK/(double)tAveV*1000.);
      #else
      Serial.print(tMaxV/2);
      Serial.print(",");
      Serial.print(tMinV/2);
      Serial.print(",");
      Serial.println(tAveV/2);  // これで単位がusになる
      #endif
    } else {
      skp--;
    }
  }
}

おまけ

実は、AC電源から信号を取ってこなくてもAC電源の周波数変動は大雑把には計測できます。(バッテリで動作しているノートPCの場合は無理かも)
ICP1端子に適当なケーブルをつないで、ICP1端子をプルアップなしの入力端子にしてやると、周りとの静電誘導にて電源周波数のノイズが回り込みます。これで一応電源周波数のモニターができます。
ただし、外部からのノイズに非常に弱いと思われますし、静電気などで破壊されるリスクも増えると思います。

#define TCCCLK 2000000    // CPUクロックを8分周した値。
                          // Pro Miniなど8MHzで動作している製品の場合は 1000000 にする
#define ICP1 8  // ICP1の端子を定義

// 周期測定ののための変数
volatile uint16_t NumV;
volatile uint32_t SumV;
volatile uint16_t CapV;
volatile uint16_t PreCap,NowCap;

// 1秒間の最大最小平均を格納するための変数
uint16_t tNumV;
float tAveV;
uint32_t tSumV;

unsigned long current_time;

// 割り込みハンドラ
ISR(TIMER1_CAPT_vect) {
  TIFR1 |= (1<<ICF1); 
  NowCap = ICR1;
  CapV = NowCap - PreCap;       // 差分を求めて周期を算出
  PreCap = NowCap;
  SumV += CapV;
  NumV++;
}

void setup()
{
  Serial.begin(38400);
  pinMode(LED_BUILTIN, OUTPUT);
  
  // TCC1でICP1信号(PB0端子=IO8)の周期を計測する
  TCCR1A = 0; //initialize Timer1
  TCCR1B = 0;
  TCNT1 = 0;
  
  pinMode(ICP1, INPUT);  //ICP1の端子をプルアップなしの入力に設定する

  TCCR1B =  0x02;   // 16MHzを8分周
  TIMSK1 |= (1 << ICIE1); // キャプチャ割り込み許可
  current_time = millis();
}

#define FREQ  // 計測を周波数で行う場合

void loop()
{
  static uint8_t skp = 3;   // 最初の何回かはデータを捨てる
  
  if (millis() - current_time >= 1000)
  {
    // 割り込みで更新している変数をコピー&初期化する
    noInterrupts(); //割り込み停止
    tSumV = SumV; tNumV = NumV;
    SumV = 0; NumV = 0;
    interrupts();   //割り込み再開
    tAveV = (float)tSumV / (float)tNumV;
    
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    current_time = millis();

    // PCに送信
    if(skp == 0){
      #ifdef FREQ
      Serial.println((double)TCCCLK/(double)tAveV*1000.);
      #else
      Serial.println(tAveV/2);  // これで単位がusになる
      #endif
    } else {
      skp--;
    }
  }
}

Sensirion SCD30を試す

パチもののMH-Z19Bにがっくり来てしまい、いろいろググっていたところ、SensirionのSCD30というCO2センサがあることを知りました。精度も高いようで価格もMH-Z19Bが3個分くらいで手が出ない価格ではありません。この微妙な価格付けとパチものショックで、つい手に入れてしまいました。

テスト環境を作る

テスト環境はまたSeeeduino Xiao + Arduino IDEで作ることにしました。まずはテストなのでキャラクタ液晶で動かします。液晶はとりあえず秋月で売っているAQM0802Aとピッチ変換基板を使うことにしました。チープなキャラクタ液晶の表示は嫌いなので、最終的にはグラフィック液晶にします。ピッチ変換基板上でI2Cバスのプルアップを有効にしておきます。

液晶表示ライブラリはオレ工房さんのライブラリを使わせていただくことにして、テストしてみました。サンプルプログラムはもちろん動いたので、少し改造して遊んでみましたが、問題なく動きます。

SCD30を動かしてみる

SCD30のライブラリはSparkfunのSCD30 Arduino Libraryを使うことにします。これはライブラリマネージャからSCD30で検索するとすぐに見つかります。

まずは、電源(3.3V)、GND、SCL、SDAを接続して、サンプルプログラム Example1_BasicReadingsを動かしてみます。

こんな感じであっけなく動作しました。

キャラクタ液晶に表示してみる

サンプルプログラムにキャラクタ液晶に表示するコードと追加してみました。

湿度の表示が24%と出ていますが、これは温度を表示しちゃっていました。あと、最初の頃に1000ppmオーバーの表示をしていた名残(?)で、ppmのmが2個表示されちゃっていますが、無事に動きました。

ソースコードはオレ工房さんのライブラリSparkfunのSCD30 Arduino Libraryがよくできているので、とてもシンプルです。

#include <Wire.h>

// 液晶ライブラリ
#include <ST7032.h>
ST7032 lcd;

// SCD30ライブラリ
#include "SparkFun_SCD30_Arduino_Library.h"
SCD30 airSensor;

void setup()
{
  // 液晶サイズ設定&コントラスト設定
  lcd.begin(8, 2);
  lcd.setContrast(32);
  // メッセージ表示
  lcd.setCursor(0, 0);
  lcd.print("SCD30");
  lcd.setCursor(0, 1);
  lcd.print(" CO2 SNS");
  
  Serial.begin(115200);
  Serial.println("SCD30 Sensor Test Program");
  Wire.begin();

  if (airSensor.begin() == false)
  {
    Serial.println("Air sensor not detected. Please check wiring. Freezing...");
    while (1)
      ;
  }

}

uint16_t co2;
float temp,hum;

void loop()
{ // SCD30からデータを読み出せる場合にはシリアルとキャラクタ液晶に表示する
  if (airSensor.dataAvailable())
  {
    co2 = airSensor.getCO2();
    Serial.print("co2(ppm):");
    Serial.print(co2);
    lcd.setCursor(0, 0);
    lcd.print(co2);
    lcd.print("ppm ");

    temp = airSensor.getTemperature();
    Serial.print(" temp(C):");
    Serial.print(temp, 1);
    lcd.setCursor(0, 1);
    lcd.print(temp,1);

    hum = airSensor.getHumidity();
    Serial.print(" humidity(%):");
    Serial.print(hum, 1);
    lcd.setCursor(5, 1);
    lcd.print(hum,0);
    lcd.print("%");

    Serial.println();
  }
  else
    Serial.print(".");

  delay(500);
}

MH-Z19Bで作成した2つと並べてみました。

右下が本物のMH-Z19Bで作ったもの、右上がパチもののMH-Z19Bで作ったもの、左が今回のSensirion SCD30です。窓を開けてしばらく待った結果ですが、400ppmまでは下がっていません。

SCD30はケースがないせいもあって応答速度がかなり速く、息を少し吹きかけただけでも大きく値が変動します。また、こうしてみると、パチもんのMH-Z19Bでも目安としては使えそうな気がしてきます(笑)。

とりあえず、SCD30は動作することがわかったので、グラフィック液晶での表示の作り込みとケースへの組み込みを考えてみようかと思います。

CO2モニタ2号機の作成(3)〜フェイク品掴んでしもた・・・

さて、いつまでたってもオートキャリブレーションが終わらないので、「室内だと400ppm近傍までなかなかさがらないからかな?」と思って、窓の外に出して、TTGO T-Cameraで監視しています。

しかし、数値は全然収まる気配がありません。

で、この記事を書いている際にふと目にしてしまいました。

「MH-Z19Bにフェイク品がある」

ということを。こちらのページにまとめがあり、主な違いとして、

  • 純正品はケースの形状が複雑(外気取り入れ部の外周が出っ張っている。また上面に円形の出っ張っている部分がある)が、フェイク品はそのような形状がない
  • 純正品は基板の色が緑色でRoHSのマーキングがあるが、フェイク品の基板は黒色

ということがあるようです。で、確かに初号機の記事に写っているMH-Z19Bの写真は、

なのですが、2号機のMH-Z19Bの写真は

なので、見事にフェイク品に該当しています。うーん・・・(T_T)

見比べると、純正品のラベルの2次元コードはSemacodeのようなのですが、フェイク品はQRコードになっています(しかもまともに読めない・・)。
2回ともAliExpressの同じShopから買いましたが、1回目は本物だったものの、2回目はフェイク品を掴まされてしまったようです。

フェイク品に関する情報が記載されているURL

CO2モニタ2号機の作成(2)

CDSのテスト

CDSは最終的に使用しませんでしたが、以下のプログラムでテストしました。

int sensorPin = A4;
int sensorValue = 0;
int powerPin = 5;

void setup() {
  pinMode(powerPin, OUTPUT);
  digitalWrite(powerPin, HIGH);
  Serial.begin(9600);  // 速度は意味はない
}

void loop() {
  // CDSへの電源を供給(HIGH)してAD変換、完了後にCDSへの電源をOFF(LOW)にする
  digitalWrite(powerPin, HIGH);
  sensorValue = analogRead(sensorPin);
  digitalWrite(powerPin, LOW);
  delay(0.1);
  // 電圧値に変換
  float voltage = sensorValue * (3.3 / 1023.0);
  // デバッグ出力
  Serial.print(sensorValue);
  Serial.print("  ");
  Serial.println(voltage);
}

LCDへの表示

LCDへの表示はこちらのpcd8544ライブラリを使用しました。

測定値を表示する大きな文字については、LinuxMintに入っているfreefontのうち使用する数字(とスペース)を以下の手順でCの配列と互換のあるxbm形式に変換した上で編集して、ソースコードに取り込んで使用しました。取り込んだものを起動時にpcd8544ライブラリで表示できるビットマップに変換しています。

$ for i in {0..9};do convert -background white -fill black -font /usr/share/fonts/truetype/freefont/FreeSansBold.ttf -pointsize 36 label:"${i}" d${i}.xbm; done
$ cat d?.xbm >> fonts.c

実際に表示させると、convertで変換する際に(おそらく)誤差拡散アルゴリズムを使用して2値ビットマップに変換しているので、その際に文字のエッジにギザギザがでますが、今回はそのまま使用しています。

MH-Z19Bの制御

以前使ったこちらのライブラリをそのまま使用することにしました。これらでブレッドボード状態でソフトウェアのデバッグを進めました。

ブレッドボード上でテストしています。この時にはCDSも生きていてCDSに手をかざすと液晶のバックライトが点灯します。MH-19Bの形状に注目・・・(;_;)

これを強引に実装したのがこちらです。

裏側はこんな感じです。MH-19Bの基板の色などに着目(;_;)

これで放置するとキャリブレーションが行われるはずですが・・・(;_;)

ケースの作成

待っている間にケースを作って入れてみました。強引に実装したので、ケースの構造には悩みました。で、2回の修正を経て、こんな感じになりました。大きさは実測で84mm × 50.5mm × 23.5mm、重さは76gです。普通の名刺より少し小さめ、海外の名刺より少し短いくらいになりましたので、携帯性はOKです。・・・が、電源スイッチをつけるのを忘れてました・・・。

一方で、消費電流がやや大きく、平均でLiPoのところで見て50mAくらいはあるのではないでしょうか。LiPoも表記上は500mAhですが、実際には300mAhくらいかと思いますので、稼働時間は5時間位だと思います。

しかし、ケースを設計している際に違和感はあったんです。『1号機の設計の際にはもっとMH-Z19Bの周りは悩んだような気がするのだが??』と。 〜つづく〜

CO2モニタ2号機の作成(1)

少し前に久しぶりに電車に乗ったところ、外が寒くなったせいか明らかに以前よりも窓の開き具合が小さくなり、社内のエアコンも動かなくなっていました。そのため、車内の空気の流れも明らかに少なくなっていて、コロナの感染拡大防止という観点では環境が悪化したと感じていました。
そこで、以前から考えていたポータブル型のCO2モニタの作成に着手しました。先に作った1号機はESP32を搭載していて自力でWiFi接続してサーバにMQTTでデータをアップロードできて定点の連続観測向きではあるのですが、長さが154mmもあり、厚さも29mmと持ち運びに便利とは言い難いものでした。また、OLEDで表示ができるのですが、以前購入したTTGO T-Cameraに付属のOLEDは連続で表示し続けると点灯している画素が劣化して、表示にムラがでてきています。ですので、今回は表示は液晶を使用して表示し続けても問題がないように作りたいと思います。

ハードウェアの構成

ハードウェアは一品物ですので、以下の条件で考えました。

  • 開発(ハードウェア、ソフトウェア)が楽であること
  • 電池である程度の時間動作すること
  • 小型でポケットに入る大きさになること
  • センサーは実績のあるMH-Z19Bを使用すること
    (これは2個目をAliExpressで購入してありましたが、これが実は・・・(;_;))
  • できるだけ手持ちの部品で構成できること

これらの条件から、以下の構成としました。

  • CPUボードはSeeeduino Xiaoを使用、開発環境は Arduino
    これは秋月電子で買ったSeeeduino Xiaoをテストしてみたところ消費電流がかなり少なそうだと感じたためです。実際には、USBの消費電流を測るおもちゃが低消費電流域ではどうも値が小さめにでているようで、それで騙されていたっぽいです。ちゃんとテスターで測ると実際にはもっと大きめでした・・・。
    液晶を接続する端子を確保した上で、MH-Z19Bを接続するUART端子、明るさを測定するCDSを接続するADCの端子とCDSへの電源供給をするGPIOを確保しても端子が足りて、小型であるのも理由です。
  • 表示はジャンク箱に入っていたNokia5100液晶(昔Aitendoで買った残り)を使用
    表示するのは基本的には測定値とトレンドグラフだけにしようと思ったので、解像度が高くないグラフィック液晶、ということで使用しました。当初はCDSを使って明るさを検知し、周囲が暗い場合には液晶のバックライトを点灯しようと思っていたのですが、CDSを取り付ける実装方法が思いつかなかったためにCDSは実装していません。
  • 電源はAliExpressで購入したLiPoバッテリーの充電モジュールと5Vへの昇圧コンバータモジュールを使用
    LiPoバッテリーについては以前購入したおもちゃのクアッドコプターの予備のLiPoバッテリー(500mAh)を使用することにしました。なお、5Vへの昇圧コンバータは別に必要なことを忘れていて、実装に苦労しました。

Seeeduino Xiaoの開発環境

Seeeduino Xiaoでの開発環境はSeeedStudioのこちらのページを参照して行いました。ボードマネージャで設定するURLもこのページの真ん中あたりに記述があります。
また、Arduinoで使用する端子の記述はインストールが済むと
~/.arduino15/packages/Seeeduino/hardware/samd/1.8.1/XIAO_m0/variant.h
にあります。

結線図

めんどくさいし、回路図を書くほどのものでもないので、いつも結線図で済ませてしまいます。Seeeduino Xiaoに接続する各モジュール配線表は以下のとおりです。

Seeeduino
Xiao
ピン番号
Seeeduino Xiao
端子機能
接続先
1D0未使用
2D1NOKIA LCD SCE(pin3)
3D2NOKIA LCD RESET(pin4)
4D3NOKIA LCD D/C(pin5)
5D4/SDACDSセンサ端子(最終的には未使用)
GNDとの間に4.7kΩを接続
6D5/SCLCDSセンサ端子(最終的には未使用)
GPIOとして電源供給
7D6/TXMH-Z19B RX(pin5)
8D7/RXMH-Z19B TX(pin6)
9D8/SCKNOKIA LCD SCLK(pin7)
10D9/MISONOKIA LCD LED+(pin8)
11D10/MOSINOKIA LCD SDIN(pin6)
123V3NOKIA LCD VCC(pin1)
13GNDNOKIA LCD GND(pin2)
MH-Z19B GND(pin3)
DCDCコンバータGND
145VMH-Z19B Vin(pin4)
DCDCコンバータ出力

BeagleBone Blackでttydを動かす

BeagleBone BlackでWebブラウザ上でターミナルが使えるようになるttydを動かしてみます。

インストール

BeagleBone Blackはoverlayroot化してあるので、それを解除してからインストールします。今回はビルド済みのバイナリをwgetで持ってきて、実行属性を付けます。

debian@beaglebone:~ $ overlayroot/rootwork 
root@beaglebone:/# cd opt 
root@beaglebone:/opt# wget https://github.com/tsl0922/ttyd/releases/download/1.6.2/ttyd.armhf
(中略)
root@beaglebone:/opt# mv ttyd.armhf ttyd
root@beaglebone:/opt# chmod +x /opt/ttyd

起動

systemdで起動するための設定ファイルを /etc/systemd/system/ttyd.service として以下の内容で作成します。

[Unit]
Description=TTYD
After=syslog.target
After=network-online.target

[Service]
ExecStart=/opt/ttyd login
Type=simple
Restart=always
User=root
Group=root

[Install]
WantedBy=multi-user.target
WantedBy=network-online.target

systemd でサービス起動をenableして、rootworkから抜けてから再起動します。

root@beaglebone:/# sudo systemctl enable ttyd
root@beaglebone:/# exit
debian@beaglebone:~ $ sudo reboot

再起動してからブラウザで http://beaglebone.local:7681 にアクセスすると、シェルのログインプロンプトが出ていて、通常のシェルと同じように扱うことができます。ただし、SSLで保護はされていないので要注意です。ttydの様々な使い方がドキュメント(Wiki)にあるので参考になります。