😶‍🌫️

TinyGo + MH-Z19C(CO2 センサー) で、室内の二酸化炭素濃度を計測してみる

2025/02/23に公開

やりたいこと

タイトルの通り。データ通信には PWM と UART を使用する方法のうち、UART 側で実施する。
また、数値のデータを保存して、ある程度の妥当性は確認したい。
最終的にはディスプレイに表示したい。

結果

計測が成功し、保存した数値データから上手く動いてそうであることを確認することができた。

  • 数値をグラフ化したもの
    • 詳細:計測を開始し、屋内 → 屋外 → 屋内へ移動させた結果、屋外では 400ppm となっており、意図通りに数値が変動している(400ppm → 外気環境は一般的に400ppmとされている)

「ディスプレイに表示」については・・・、気が向いたら追記します。

背景

Arduino IDE でライブラリを使用した結果。動作することは確認した。
しかし、以下の問題が発生したため、先日セットアップしたロジックアナライザ で動作確認しつつ、TinyGo で勉強も兼ねて実装してみようと考えた。
後述しますが、発生した問題の原因は回路でした。 以下の写真に問題点が写ってます。

  • 使用した回路
  • 発生した問題点
    • 計測値が安定しない
      • 最初は問題なく表示されるが、時間経過と主に数値が下がり、最終的に室内にも関わらず、300ppm まで下がってしまった
    • PWM では動作するが、UART を使用した場合はうまく動作しない
      • UART 通信しようとするとエラーになってデータを受信することができない

やってみる

使用するもの

  • ソフト
    • TinyGo
      • tinygo version 0.33.0 windows/amd64 (using go version go1.22.6 and LLVM version 18.1.2)
  • ハード

実装する

機器の接続

こんな感じで接続しました。

Xiao RP2040 ピン MH-Z19C ピン
5V Vin
GND GND
D6 (UART0 TX) Rx
D7 (UART0 RX) Tx

ソースコード

説明はソースコード内のコメントに記載しているため、データシート を見つつ確認してもらえればと思います。

package main

import (
	"fmt"
	"machine"
	"time"
)

func main() {
	// UART の設定
	uart := machine.UART0
	err := uart.Configure(
		machine.UARTConfig{
			// センサーとの通信は 9600bps にする必要がある
			BaudRate: 9600,
			RX:       machine.UART0_RX_PIN,
			TX:       machine.UART0_TX_PIN,
		})
	if err != nil {
		fmt.Println(err)
	}

	// 待機処理:「Preheat time」に「1 min」と記載されているため、待つ
	fmt.Println("waiting preheating")
	for i := 0; i < 60; i++ {
		fmt.Println(".")
		time.Sleep(time.Second)
	}
	fmt.Println("setting end")

	// センサーに対して以下のデータを送信することで、センサーが CO2 の計測データを送信する
	sendData := []byte{
		0xFF,
		0x01,
		0x86,
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
		0x79,
	}

	// uart.READ() 実行するにあたり、センサーが送信するデータ数分の length にする必要がある
	recieveData := []byte{
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
	}

	fmt.Println("sending messages start:board -> MH-Z19C")
	for {
		uart.Write(sendData)
		uart.Read(recieveData)

		// センサーから受け取ったデータで、計測データを算出する
		dataHIGH := int(recieveData[2])
		dataLOW := int(recieveData[3])
		ppm := (dataHIGH * 256) + dataLOW

		fmt.Printf("%d\r\n", ppm)
		time.Sleep(time.Second * 2)
	}
}

余談

1. 回路の誤り
回路を変えている時に、回路内の誤りに気付いた。
これ ↓ 赤枠内全部のピンが短絡してるじゃん・・・。

幸いにも、こちら側のピンは電源はないため、壊れませんでした。
「V0」? 「SR」 という動きがよくわからないピンがありますが、恐らく、上手く動いていなかった原因は以下です。

  • 計測値が安定しない
    • → キャリブレーションを行うためのピンがずっと他のピンと繋ながっているため
      • キャリブレーションを行うには 400ppm の環境で GND ピンに繋ぎ、そこを基準にしなければならないが、CO2 濃度が高い場所でキャリブレーションされ、そこを基準にするということが発生している(例えば 700ppmの環境を基準に計測すると 400ppm の環境になった時に数値が400を下回ってしまう)
  • PWM は動作するが、UART を使用した場合はうまく動作しない
    • → UART を使用した場合の RX, TX のピンが繋がっているので通信に失敗している

2. ロジックアナライザの結果
ちょっと warning が出てますが、必要なデータのやり取りは出来ているので・・・。
問題になってそうであれば、解決に向けて頑張ったり、頑張らなかったりすると思います。

  • (マイコンの)送信側
  • (マイコンの)受信側

参考

追記(2025/02/24)

ディスプレイを追加しつつ、リファクタリングをして、最終的には以下のような感じに落ち着きました。

  • 画像
  • ソースコード
package main

import (
	"fmt"
	"image/color"
	"machine"
	"strconv"
	"time"

	"tinygo.org/x/drivers/ssd1306"
	"tinygo.org/x/tinyfont"
	"tinygo.org/x/tinyfont/freemono"
)

var (
	// センサーに対して以下のデータを送信することで、センサーが CO2 の計測データを送信する
	// 0x86- Read CO2 concentration
	SendData = []byte{
		0xFF,
		0x01,
		0x86,
		0x00,
		0x00,
		0x00,
		0x00,
		0x00,
		0x79,
	}

	// ディスプレイ用
	White = color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}
)

func main() {
	// ディスプレイの設定
	machine.I2C0.Configure(machine.I2CConfig{
		Frequency: 2.8 * machine.MHz,
		SDA:       machine.I2C0_SDA_PIN,
		SCL:       machine.I2C0_SCL_PIN,
	})
	display := ssd1306.NewI2C(machine.I2C0)
	display.Configure(ssd1306.Config{
		Address: 0x3C,
		Width:   128,
		Height:  64,
	})
	display.ClearBuffer()
	display.ClearDisplay()
	time.Sleep(time.Millisecond * 50)

	// UART の設定
	uart := getUART0()
	// 待機処理:「Preheat time」に「1 min」と記載されているため、待つ
	time.Sleep(time.Minute)
	// uart.READ() 実行するにあたり、センサーが送信するデータ数分の length にする必要があるため、注意
	recieveData := make([]byte, 9)

	tinyfont.WriteLine(&display, &freemono.Regular9pt7b, 3, 10, "60sec\n:Preheating", White)
	display.Display()

	for {
		uart.Write(SendData)
		uart.Read(recieveData)

		ppm := strconv.Itoa(calcPPM(int(recieveData[2]), int(recieveData[3])))

		display.ClearDisplay()
		tinyfont.WriteLine(&display, &freemono.Regular9pt7b, 3, 10, "PPM:", White)
		tinyfont.WriteLine(&display, &freemono.Regular24pt7b, 3, 50, ppm, White)
		display.Display()

		time.Sleep(time.Second * 2)
	}
}

func getUART0() machine.UART {
	uart := machine.UART0
	err := uart.Configure(
		machine.UARTConfig{
			// センサーとの通信は 9600bps にする必要がある
			BaudRate: 9600,
			RX:       machine.UART0_RX_PIN,
			TX:       machine.UART0_TX_PIN,
		})
	if err != nil {
		fmt.Println(err)
	}

	return *uart
}

func calcPPM(dataHIGH, dataLOW int) int {
	return (dataHIGH * 256) + dataLOW
}

Discussion