Tang NanoでLチカ

開発環境をインストールしたので引き続きTang NanoでLチカにトライします。基本的には、論理合成にライセンスファイルの請求が必要な Synplify Pro ではなく、ライセンスファイルの請求なしに動作する GowinSysthesis を使っている以外は Qiita の @cinimal さんの「Sipeed Tang Nanoで遊んでみる (Linux版)」と同じ手順です。(感謝)

プロジェクトの作成

GOWIN FPGA DesignerのStart PageでNew Projectアイコンをクリックしてプロジェクトを作成します。

FPGA Desgin Projectを選んでOKをクリックします。

プロジェクト名をつけて、プロジェクトを保存するディレクトリをセットしてNextをクリックします。

GW1N-1のQFN48ピンの中から遅い方=GW1N-1LV1QN48C6/I5を選択します。

サマリを確認してFinishをクリックします。

ソースファイルの作成

Designペインで右クリックしてNew Fileを選択。

ファイルの種類の選択からVerilogファイルを選択し、作成するファイル名と拡張子を入力、現在のプロジェクトに追加(Add to current project)する。

Tang Primerのときに作成した led.v の内容をコピペする。(Add Filesで良かったかも)

module led
	(
		input wire CLK_IN,
		input wire RST_N,
		output wire [2:0]RGB_LED
	);

	parameter time1 =    10'd1000 - 1'd1;
//	parameter time2 =    10'd1000 - 1'd1;	// 1秒周期
	parameter time2 =    10'd500 - 1'd1;	// 0.5秒周期
	
	reg [2:0] rled;
	reg [2:0] rledout;
	reg [9:0] count1;   // PWM DUTY用(1ms周期)
	reg [9:0] count2;   // PWM レベル用(1s周期)
	reg [4:0] ckec;
	reg cke;			// 1us周期イネーブル

	reg [9:0] duty;
	
	initial
	begin
		count1=10'b0;
		count2=10'b0;
		rled=3'b110;
		cke = 1'b1;
		ckec <= 5'b0 ;
	end

	// 1MHz周期のイネーブル信号生成
	always @(posedge CLK_IN)begin
		if(RST_N==0)begin
			cke = 1'b1;
		end
		if(ckec == 5'd23)
		begin
			ckec <= 5'b0 ;
			cke <= 1'b1;
		end
		else begin
			ckec <= ckec + 1'b1;
			cke <= 1'b0;
		end
    end

	always @(posedge CLK_IN)begin
		if(cke == 1'b1 & RST_N==0)begin
			count1 <= 10'b0;
			count2 <= 10'b0;
			rled <= 3'b110;
		end

		// 1usクロックで0〜999(time1)までカウント
		if(cke == 1'b1)
		begin
			if(count1 == time1)
	 		begin 
	 			count1<= 10'd0;
	 		end
	 		else begin
	 			count1 <= count1 + 1'b1;
			end
	 	end

		// count1の1周期で0〜999(time2)までカウント
		if(cke == 1'b1 & count1 == time1)
			begin
				if(count2 == time2)
		 		begin 
		 			count2<= 10'd0;
					rled <= {rled[1:0],rled[2]};
		 		end
		 		else
		 			count2 <= count2 + 1'b1;
		 	end

		// LEDを駆動するPWM信号生成
		if(cke == 1'b1)
		begin
			if(count2 < time2/2)
				duty = count2 * 2;
			else 
				duty = (time2 - count2) * 2;
	
			if(count1 < duty)
				rledout = rled;
			else
				rledout = 3'b111;

		end
	end

	assign RGB_LED = rledout;

endmodule

合成と端子制約の追加

ProcessペインからSynthesizeをダブルクリックして、一旦合成を実行

保存されていなかったので、保存するファイルを確認してOKをクリック。

合成が実行される。

ProcessタブでFloorPlannerをダブルクリックする。

端子制約ファイル(*.cst)がないので作成するか聞いてくるのでOKをクリック。

チップ内のレイアウトが開くので、Package Viewタブをクリック、下のペインでI/O Constraintsをクリックする。

端子を割り当てる画面になります。

上記のTang Nanoの回路図はTang NanoのドキュメントのページDownload Stationの中のHDKディレクトリの中にあります。これを見ると、

  • クロックジェネレータからのクロックはIOR5A/GCLKT_2/RPLL_T_in(35pin)
  • LED_GはIOB7A(16pin)
  • LED_BはIOB10A/GCLKT_5(17pin)
  • LED_RはIOB10B/GCLKC_5(18pin)
  • ボタンAはIOB6B(15pin)
  • ボタンBはIOB3B(14pin)

につながっているようなので、PortのCLK_INをチップの絵の35ピンへドラッグアンドドロップします。
同様にLEDの3本を16〜18ピンへドロップ、RST_Nを15ピンへドロップします(下記では間違ってRST_Nを13ピンへつないでしまっています)。I/O電圧はすべて3.3Vのレギュレータへつながっているので、IO TypeをLVCMOS33にセット、LEDの3つの端子はアノードコモンで3.3Vに吊られているので気分でOpen Drainにセット、Drive能力は4mAあれば十分なので4にセット、プルアップはすべてNONEにセットしました。

フロッピーのアイコンで保存して、FloorPlannerを閉じます。

タイミング制約の追加

ProcessペインのTiming Constraint Editorをダブルクリックします。

Timing Constraint Editorが開きます。

右画面で右クリックしてCreate Clockを選択します。

Clock nameにCLK_IN、Frequencyに24[MHz]をセットします。

Searchを押して信号を検索、CLK_INを選択して「>」で対象にします。

OKを押します。

OKを押してクロックのリストに追加されていることを確認します。

フロッピーのアイコンをクリックして保存します。

配置配線を実行

ProcessペインでPlace&Routeをダブルクリックして実行します

実行すると、チェックマークが付きます。

ビットストリームの書き込み

一般ユーザーでもプログラマが使えるようにudevの設定がしてあれば、Tang Nanoを接続した後、

$ sudo modprobe -r ftdi_sio

としてから、Tools→Programmerでプログラマを起動します。modprobeでモジュールを外さないとデバイスが見つからないのでこの手順は重要です。

虫眼鏡のマークのScan Deviceでデバイスをサーチします。

GW1N-1を選択します

JTAGチェーンにGW1Nが追加されました。

デバイスをダブルクリックして、設定を行います。

OperationはSRAM Programを選択し、Programming Optionでビットストリームファイルを選択します。プロジェクトディレクトリの impl/pnr ディレクトリの下に拡張子 fs でできているはずです。

確認してSaveしてから、Program/Configureボタンをクリックします。

すると、プログレスバーが出てきて、書き込みが完了します。必要に応じて回路で設定したリセットボタンを押して動作させます。

ここで、Tang Primerでは

	assign RGB_LED = rledout;

という記述をしていた assign 文は Tang Nanoではだめでしたので、

	assign RGB_LED[0] = rledout[0];
	assign RGB_LED[1] = rledout[1];
	assign RGB_LED[2] = rledout[2];

と修正したら問題なく動作しました。(でもエラーは出ないので、悩みました)

実際の操作の際は、FloorPlannerやProgrammerは開いたままでソース修正⇒合成⇒書き込みができますので、まあまあ作業性は良さそうです。

Tang Nano 開発環境をインストール

Tang Primerに引き続き、さらに格安なFPGAボードTang Nanoの開発環境もインストールしてみました。ShigeZoneさんで800円で買えてしまうし、よく見るとDIP40ピンのソケットに挿せそうだし、ソフトウェアでできないことにトライするにはお手軽かなぁ、と思って挑戦してみました。

基本的には@cinimlさんの「Sipeed Tang Nanoで遊んで見る(Linux版)」に沿って始めてみました。しかし、現在ではSynplify Proのライセンスチェックが厳しくなっており、この手順のままではSynplify Proが動作しませんでした。(まあ、当たり前でしょうね・・・)

基本的な手順は変わっていませんが、GowinSynthesisであればライセンスの請求をすることなく動作させることができましたので、その手順をメモしておきます。環境はLinux Mint 20です。

GOWIN EDAのダウンロード

開発に使用するGOWIN EDAはこちらからダウンロードできました。ダウンロードにあたっては、ユーザー登録が必要で、登録にはメールアドレスと電話番号が必要でした。登録してログインすると、「Gowin EDA」のダウンロードのところからダウンロードできるようになっています。

自分は間違って一つ古い Gowin_V1.9.7.05Beta_GowinSynthesis-only_linux をダウンロードしてしまいましたが、最新版でも同じでしょう。GowinSynthesis-onlyのない版は Symplify Pro が使えるものですが、「Gowin EDA」のダウンロードの下にあるライセンスの申請からライセンスを申請する必要があるようです。(GowinSynthesis-onlyはライセンス申請の必要がない)

GOWIN EDAのインストール

ダウンロードしたファイルをディレクトリを作って展開します。

~$ mkdir gowin
~$ cd gowin/
~/gowin$ tar xvfz ~/ダウンロード/Gowin_V1.9.7.05Beta_GowinSynthesis-only_linux.tar.gz 

で、

~/gowin$ IDE/bin/gw_ide 

として起動するとライセンスエラーになりますので、ライセンスサーバーを設定して、Test ConnectionをクリックしてからSaveします。

その上で、再度

~/gowin$ IDE/bin/gw_ide 

として起動すると、

として起動することができました。Synplify Proが使えるバージョンは FloorPlannerの横にSynplify Proのアイコンがありますが、アイコンをクリックしてライセンス同意をしても、その後でライセンスエラーになります。

一般ユーザーでもProgrammerを使えるようにする

以下の手順を行って、一般ユーザーでもProgrammerを使えるようにしておきます。これは@cinimlさんの「Sipeed Tang Nanoで遊んで見る(Linux版)」の手順のままです。

~$ cd /etc/udev/rules.d/
/etc/udev/rules.d$ cat 50-tang-nano.rules 
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", ATTRS{product}=="Sipeed-Debug", GROUP="users", MODE="0666"
/etc/udev/rules.d$ sudo udevadm control --reload
/etc/udev/rules.d$ sudo modprobe -r ftdi_sio

ここでTang Nanoを挿入して、テストします。

$ cd ~/gowin/
~/gowin$ Programmer/bin/programmer_cli --channel 0 --device GW1N-1 --operation_index 0
 "Read Device Codes" starting on device-1...
 Unknown Cable
 ID code is: 0x0900281B
 User code is: 0x0000496F
 Status code is: 0x0001F020
 Cost 0.66 second(s)

Tang PrimerでPWMでLチカ

LEDのサンプルを改造して、PWMでホワッと点滅するLチカにしてみました。

秒を基本とした周期にするため、まず24MHz入力クロックを24分周して、1MHz周期のイネーブル信号ckeを生成します。

これで0〜999(time1)までカウントするcount1と、count1の桁上がりのタイミングで0〜499(time2)までカウントするcount2を作ります。

count2が前半の場合には2倍した値を、後半の場合にはtime2から引いた値を2倍した値をduty比較用の値として count1 と比較して、点灯期間の場合には指定のLEDを点灯、消灯期間の場合には全LEDを消灯させます。

/* デバイスは EG4::EG4S20BG256 */ 

module led
	(
		input wire CLK_IN,
		input wire RST_N,
		output wire [2:0]RGB_LED
	);

	parameter time1 =    10'd1000 - 1'd1;
//	parameter time2 =    10'd1000 - 1'd1;	// 1秒周期
	parameter time2 =    10'd500 - 1'd1;	// 0.5秒周期
	
	reg [2:0] rled;
	reg [2:0] rledout;
	reg [9:0] count1;   // PWM DUTY用(1ms周期)
	reg [9:0] count2;   // PWM レベル用(1s周期)
	reg [4:0] ckec;
	reg cke;			// 1us周期イネーブル

	reg [9:0] duty;
	
	initial
	begin
		count1=10'b0;
		count2=10'b0;
		rled=3'b110;
		cke = 1'b1;
		ckec <= 5'b0 ;
	end

	// 1MHz周期のイネーブル信号生成
	always @(posedge CLK_IN)begin
		if(RST_N==0)begin
			cke = 1'b1;
		end
		if(ckec == 5'd23)
		begin
			ckec <= 5'b0 ;
			cke <= 1'b1;
		end
		else begin
			ckec <= ckec + 1'b1;
			cke <= 1'b0;
		end
    end

	always @(posedge CLK_IN)begin
		if(cke == 1'b1 & RST_N==0)begin
			count1 <= 10'b0;
			count2 <= 10'b0;
			rled <= 3'b110;
		end

		// 1usクロックで0〜999(time1)までカウント
		if(cke == 1'b1)
		begin
			if(count1 == time1)
	 		begin 
	 			count1<= 10'd0;
	 		end
	 		else begin
	 			count1 <= count1 + 1'b1;
			end
	 	end

		// count1の1周期で0〜999(time2)までカウント
		if(cke == 1'b1 & count1 == time1)
			begin
				if(count2 == time2)
		 		begin 
		 			count2<= 10'd0;
					rled <= {rled[1:0],rled[2]};
		 		end
		 		else
		 			count2 <= count2 + 1'b1;
		 	end

		// LEDを駆動するPWM信号生成
		if(cke == 1'b1)
		begin
			if(count2 < time2/2)
				duty = count2 * 2;
			else 
				duty = (time2 - count2) * 2;
	
			if(count1 < duty)
				rledout = rled;
			else
				rledout = 3'b111;

		end
	end

	assign RGB_LED = rledout;

endmodule

Tang Primer 開発環境をセッティング

FPGA開発をしたくなったので、以前買ってあった Tang Primer用の環境をセットアップします。

公式サイトはこちらのようです。

セッティングにあたっては以下のサイトを参考にさせていただきました。

http://galaxystar.image.coocan.jp/tangprimer.htm

IDEをインストールと起動

https://tang.sipeed.com/en/getting-started/requirements/ にリンクがあるので、そこから下の方にあるURLからIDEをダウンロードします。

今回は、TD1909_linux.rarをダウンロードしてみました。(もっと新しい日付のものはRHEL用っぽいので)

https://tang.sipeed.com/en/getting-started/installing-td-ide/linux/ のページの記述を参考にインストールしますが、いろいろ違います。

~/ダウンロード$ sudo unrar x TD1909_linux.rar -d /opt/
~/ダウンロード$ sudo ln -s /opt/TD_RELEASE_September2019_r4.6.2/bin/td /usr/bin/td
~/ダウンロード$ sudo chmod a+x /opt/TD_RELEASE_September2019_r4.6.2/bin/td
~/ダウンロード$ td -gui

実際に起動するにはFPGAのデバイス情報を /usr/arch で参照できないといけないようです。

$ sudo ln -s /opt/TD_RELEASE_September2019_r4.6.2/arch /usr/arch 

また、ライセンスファイルが必要なようで、 https://dl.sipeed.com/shareURL/TANG/Premier/IDE から最新のライセンスファイル(Anlogic_*.lic)をダウンロードしてきて、 /usr/license の下に Anlogic.lic のファイル名で配置します。

$ cd /usr
$ mkdir license
$ sudo mkdir license
$ cd license/
$ sudo cp ~/ダウンロード/Anlogic_20220130.lic Anlogic.lic

これでIDEが起動できました。

実際には、https://dl.sipeed.com/shareURL/TANG/Premier/IDE により新しいバージョンがあるようなので、そちらを試したほうがいいかもしれません。

USBドライバをインストール

https://tang.sipeed.com/en/getting-started/installing-usb-driver/linux/ のページに沿ってドライバをインストールします。

Tang Nano PrimerをPCに接続して、lsusbで VID:PID = 0547:1002 を認識しているか確認します。

$ lsusb
Bus 001 Device 003: ID 0547:1002 Anchor Chips, Inc. Python2 WDM Encoder

USB3.0のHUB経由では認識せず、PCのポート直結、もしくはUSB2.0のHUB経由でなければ認識されませんでした。(PCのUSB3.0のポート直結なら認識するのかは試していません)

記述の通りにudevルールを作成します。

$ ls -la /etc/udev/rules.d/91-anlogic-jtag.rules 
-rw-r--r-- 1 root root 108  6月 20 08:31 /etc/udev/rules.d/91-anlogic-jtag.rules
$ cat /etc/udev/rules.d/91-anlogic-jtag.rules 
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0547", ATTRS{idProduct}=="1002", \
  GROUP="plugdev", \
  MODE="0660"

$ sudo service udev restart

次に、正しく認識しているかを確認しますが、自分の環境では一度再起動しないとだめでした。(ログアウト→ログインでも大丈夫かは試していません)

$ td -gui

として、IDEを起動します。起動したら、赤で囲った Download ボタンをクリックすると、Downloadウインドウが開くことを確認します。

ドキュメントでは、「未知のバグによりJTAGは400kbpsかそれ以下でないと動作しない」とのことです。

サンプルの合成

https://tang.sipeed.com/en/getting-started/getting-to-blinky/ に沿ってサンプル blinky を動かしてみます。

gitをインストールした上で、githubからサンプルをダウンロードしてきて、IDEを起動します。

~$ mkdir TangPrimer
~$ cd TangPrimer/
~/TangPrimer$ sudo apt install git
~/TangPrimer$ git clone --recursive https://github.com/Lichee-Pi/Tang_FPGA_Examples
~/TangPrimer$ td -gui

Project -> Open Project で ~/TangPrimer/Tang_FPGA_Examples/0.LED/prj の下の led.al を開きます。Process -> Run またはメニューボタンの中の Run をクリックすると、論理合成とフィッティングが行われて led.bit が生成されます。

サンプルのテストと改造

Tool -> Download またはメニューボタンの中の Download をクリックして、Downloadウインドウを開きます。「Add」アイコンをクリックして、生成した led.bit を選択します。Mode SelectionがJTAGになっていると、直接FPGAに書き込みに行くので、電源再投入で消えるとのことです。(http://galaxystar.image.coocan.jp/tangprimer.htm の記載による)

Speedを3Mbpsにして、Runボタンをクリックすると書き込みが行われます。書き込みが終わると動作を開始しますが、ユーザー回路へのリセットがかからないので、「USER」ボタンを押してユーザー回路へのリセットをかけてやる必要があります。以下のように書き換えて動作させようとしたものの思ったとおりに動作せず、少々悩みました。

①led.v

/* デバイスは EG4::EG4S20BG256 */ 

module led
	(
		input wire CLK_IN,
		input wire RST_N,
		output wire [2:0]RGB_LED
	);

	parameter time1 = 25'd12000000;//クロック周波数 24Mhz

	reg [2:0]rledout;
	reg [24:0] count;
	
	initial
	begin
		count=25'b0;
		rledout=3'b110;
	end

	always @(posedge CLK_IN)begin
		if(RST_N==0)begin
			count <= 25'b0;
			rledout <= 3'b110;
		end
	
		if(count == time1)
 		begin 
 			count<= 25'd0;
			rledout <= {rledout[1:0],rledout[2]};
 		end
 		else
 			count <= count + 1'b1;
	end
	
	assign RGB_LED = rledout;
endmodule

②制約ファイル io.adc

set_pin_assignment {CLK_IN} { LOCATION = K14; }		##24MHZ
set_pin_assignment {RST_N} { LOCATION = K16; }		##USER_KEY

## RGB LEDs, 3 pins
set_pin_assignment {RGB_LED[0]} { LOCATION = R3;  }	##LED_R, R3
set_pin_assignment {RGB_LED[1]} { LOCATION = J14;  }	##LED_G, J14
set_pin_assignment {RGB_LED[2]} { LOCATION = P13;  }	##LED_B, P13

空間放射線量

中国広東省の原子力発電所で放射能漏れがあった、という報道がありました。広東省は日本の西方にありますので、放射能漏れの場合には偏西風に乗って日本でも放射線が検出される可能性があります。

そこで、原子力規制委員会が公開しているここ1週間の放射線モニタリング情報をグラフ化してみました。全国のを・・・と思ったのですが、福島県はデータ提供されている地点が多すぎて、1週間分をまとめてダウンロードすることができないので福島県を除いてやってみました。

データは県ごとにまとめてダウンロードして、それをPythonで並べ替えたものをLibreOffice Calcで読み込ませてグラフを作成してみました。どのグラフがどの地域かは、グラフのキャプションに記載しています(文字色もグレーなので見にくいですが)。

結果を見ると、関東地方で6/8の夜に値の上昇が見られますが、このタイミングでは他の地域では値の上昇は見られません。また、甲信越、中部東海、関西、中国四国にかけて6/13の午前中と、6/14の早朝に値の上昇が見られるようです(関東地方でも上昇している地域はあるようです)。いずれも大きな上昇とは言い難いので、これが広東省の原発と関係があるかはなんともいえないところかと思います。

なお、一時的な値の上昇は、降雨と関係があるというご指摘をいただきました。(そういえば、そういうデータを何度も見たことがあったのですが、忘れていました)
ちょうど梅雨時ですので、そういう関係かもしれません。

東北北海道(福島県を除く)
関東地方
甲信越
中部東海
関西
中国四国
九州沖縄

ESP-Nowをためす

電池動作での長時間化を狙って、ESP8266/ESP32での直接通信モードのESP-Nowを試してみます。

ESP-NowはEspressifの説明によれば、「ESP-NOWは、Espressifによって開発されたさらに別のプロトコルであり、Wi-Fiを使用せずに複数のデバイスが相互に通信できるようにします。このプロトコルは、ワイヤレスマウスによく使用される低電力2.4GHzワイヤレス接続に似ています。したがって、デバイス間のペアリングは、通信の前に必要です。ペアリングが完了すると、接続は安全でピアツーピアになり、ハンドシェイクは必要ありません。」(Google翻訳による)ということで、Wi-Fiのような複雑なプロトコルではないので、通信にかかる時間が短く=消費電力が削減されることが期待できます。

ESP-Nowのドキュメントはこちらになるのですが、今回は簡単にArduino環境のライブラリSimpleEspNowConnection(ライブラリマネージャからインストールできる)を使って、送信側にESP8266、受信側にESP32で動作させました。ESP-Nowでespressifから提供されているAPIはESP8266とESP32で異なっているのですが、SimpleEspNowConnectionはその差分も吸収してくれます。

想定する使い方は、電池で動くセンサー側から、PCもしくはRaspberry Piに接続されたホスト側に測定データを送信する、という使い方です。本来、ESP-Nowは双方向で通信ができるのですが、今回はこの使い方に絞った形でテストしてみました。

まずは受信側のコードです。これは、ライブラリのサンプルプログラム(SimpleEspNowConnectionServer.ino)をさらに簡略化したもので、ESP32で動作させました。

#include "SimpleEspNowConnection.h"

SimpleEspNowConnection simpleEspConnection(SimpleEspNowRole::SERVER);

String clientAddress;

void OnMessage(uint8_t* ad, const uint8_t* message, size_t len)
{
  Serial.printf("MESSAGE:[%d]%s from %s\n", len, (char *)message, simpleEspConnection.macToStr(ad).c_str());
}

void setup() 
{
  Serial.begin(115200);
  Serial.println();

  simpleEspConnection.begin();
  simpleEspConnection.setPairingBlinkPort(2);
  simpleEspConnection.onMessage(&OnMessage);  

  Serial.println(WiFi.macAddress());    
}

void loop() 
{
  simpleEspConnection.loop();
}

次に送信側のコードです。これも、ライブラリのサンプルプログラム(SimpleEspNowConnectionClient.ino)をさらに簡略化したもので、テストではESP8266で動作させました。

#include "SimpleEspNowConnection.h"

SimpleEspNowConnection simpleEspConnection(SimpleEspNowRole::CLIENT);

String serverAddress;

void OnSendError(uint8_t* ad)
{
  Serial.println("SENDING TO '"+simpleEspConnection.macToStr(ad)+"' WAS NOT POSSIBLE!");
}

void OnSendDone(uint8_t* ad)
{
  Serial.println("SENDING TO '"+simpleEspConnection.macToStr(ad)+"' WAS DONE!");
}

void setup() 
{
  Serial.begin(115200);
  Serial.println();

  Serial.println("Setup...");

  simpleEspConnection.begin();
  simpleEspConnection.setPairingBlinkPort(2);  

  serverAddress = "012345ABCDEF"; // 受信側のMACアドレス
  simpleEspConnection.setServerMac(serverAddress);
  simpleEspConnection.onSendError(&OnSendError);  
  simpleEspConnection.onSendDone(&OnSendDone);
  
  Serial.println(WiFi.macAddress());  
}

void loop() 
{
  static int n=1;
  static char buf[32];
  simpleEspConnection.loop();

  sprintf(buf,"%d",n++);
  simpleEspConnection.sendMessage(buf);
  delay(1000);
}

これで実際に動作させることができました。

送信に成功した場合、失敗した場合でそれぞれコールバック関数が呼ばれるので、それに応じた処理を記述することができそうです。例えば、成功した場合には2分間のディープスリープ、失敗した場合には5秒間のディープスリープとして再トライさせるなどの使い方になると思います。

安価なCO2センサの比較(4)

SCD30もSelf Calibrationが動作する連続稼働7日を越えましたので、改めて各CO2センサの比較をしてみたいと思います。

まず、CO2濃度が上昇していく局面です。

①の前は設置環境は無人かつ窓が開いている状態で値は安定しています。①で自分が入室、②で窓を閉めました。ここから値がどんどん上昇し、③で窓を開けて換気しました。

結果的には前回とよく似た結果です。パチもんのMH-Z19Bは相変わらず大きく外れた値が出ています。

次に、400ppm付近の挙動です。(下記④)

SCD30は400ppm以下の値もそのまま出してしまうようです。
MH-Z19Bは内部の計算結果が400ppm以下の場合には400ppmでクリップ、偽物のMH-Z19Bは400ppmを下回ったらとりあえず50足しとけという乱暴な挙動に見えます。

興味深いのはMH-Z19CとSCD41の挙動が結構似ているようにみえることです。結果も今回テストした2台に関してはほぼ同じ値が出ているように見えますし。

外気のCO2濃度は気象庁の資料(二酸化炭素濃度の経年変化二酸化炭素濃度の観測結果)を見ると、410〜420ppm前後ということになります。温室内や森の中などでは周囲より下がることがありますが、通常の環境では400ppmを下回ることはありません。

安価なCO2センサの比較(3)

現時点でのCO2センサの勝手な比較をしてみました。各センサの(ほぼ)同一条件での出力比較はこちらの記事を見てみてください。
方式としては測定値に信頼性がおけるNDIR方式のもののみです。(MOX方式のCCS811も試したことがありますが、自分的には使い物になりません)
また、Sensirionのセンサはまだ初期キャリブレーションが終わっていないので、後で評価を更新するかもしれません。

利点欠点独断評価
SCD30
(Sensirion)
電源に対する要求が緩い
単体で扱いやすい
ユニバーサル基板にも載せやすい
千石マルツなどの国内の通販でも買える
低い濃度領域で400ppm以下の値が返ってくる。
初期のオートキャリブレーションの期間が長い(1週間連続通電が必要)
価格が高め。
SCD41
(Sensirion)
大きさが非常に小さい
電源に対する要求が緩い
公式ドライバソフトウェアが公開されている(ラズパイ、Arduino、組込)
省電力の間欠動作モードがある(電池動作向け)
SCD41単体での入手がまだできない?
SCD41単体でははんだ付けが困難
(評価キットで入手するのがおすすめ)
オートキャリブレーションの期間は不明(調査中)
MH-Z19B
(Winsen)
安価($20程度)
オートキャリブレーションの期間が短い(24時間)
ディスコン済み?
電源の要求条件が厳しい(5V±0.1V)
端子配置が一般的なユニバーサル基板のピッチにあってない
MH-Z19C
(Winsen)
安価で入手性良好(2480円で秋月で買える
オートキャリブレーションの期間が短い(24時間)
AliExpressに直営店出てます
電源の要求条件が厳しい(5V±0.1V)
端子配置が一般的なユニバーサル基板のピッチにあってない
MH-Z19Bの
パチもの
精度が極めて悪くて使い物にならない
価格も本物と変わらない
AliExpressで本物のMH-Z19Bの写真が貼ってあるのに送られてきたのはこれ
(Aliで買うならWinsenのShopで買いましょう)
×

秋月にはMH-Z14Bも取り扱いがあるようですが、入手していないので掲載していません。

安価なCO2センサの比較(2)

実際に手持ちのNDIR方式のCO2センサを概ね一箇所にまとめて置いて6時間ほどかけて比較してみました。場所は窓に近くで窓から50cmくらいのところで、自分が作業している椅子からは背後方向1mくらいのところです。対象は

  • Sensirion SCD30(注:キャリブレーションの期間が明らかに終わっていません)
  • Sensirion SCD41(これも稼働を始めて2日程度です)
  • Winsen MH-Z19B
  • Winsen MH-Z19C
  • MH-Z19Bのパチもの(グラフではFAKE19B)

の5つです。早速、結果から。

①の前は部屋の窓を少し開けた状態。①で窓を大きく開けました。②で窓を完全に締めました。③で暑くなってきたのでエアコンで冷房を入れました。④でエアコンを切って、窓を全開にして空気を入れ替えです。

②まではどれも同じくらいの値ですが、SCD30は外の風(ごく弱く、ほぼ無風です)の向きなどで影響されているのか、キャリブレーションが終わっていないからなのか、時々値が変動しています。窓を完全に閉めて部屋を締め切ると自分の呼気でCO2濃度が上昇を始めます。③でエアコンを稼働すると、エアコンのON/OFFに連動するのか、Sensirionのセンサーは定期的に値が上下しています。④で窓を開けると、一斉に値が下がり、400〜500ppmまで下がります。パチもんのMH-Z19Bだけは、値が大幅に異なっていて、他のセンサーでは2600ppm前後に達していても、こいつだけは1750ppm程度になっています。

というわけで、

パチもんのMH-Z19Bは使えねぇ

ということがよくわかりました。

安価なCO2センサの比較(1)

Sensirion製センサも動くようにしたので、それぞれどんな関係にあるか、見たくなってきました。
ですので、以前作った電源周波数観測用のサーバプログラムを改造して、chart.jsでグラフを描かせるようにしてみました。

本当はリアルタイム更新のほうが見た目には面白いのですが、 asyncio を使いこなせていないせいか、うまく行かず、Javascriptで強制リロードする方式にしました。

各センサからのデータ収集はUSBシリアル経由で行っていますが、今までは数値だけだったり、デバッグ情報が混ざっていたりとバラバラだったので、フォーマットを以下のように規定しました。

$(センサ名) Co2:(CO2濃度値 ppm) ・・・(その他の情報)

各USBシリアルから入ってくるデータを読んで、蓄積、定期的にHTML出力するコードとしました。(とにかく動けばいいや、なのでテキトーです)

# -*- coding:utf-8 -*-
#
import os.path,sys
import serial,time,re

# webserverは
#   python -m http.server 8080
# で別途起動

hist = {}        # データを保持する変数
hlen = 60000       # データを保持する数

devs = (
    '/dev/ttyUSB0',
    '/dev/ttyUSB1',
    '/dev/ttyUSB2',
    '/dev/ttyUSB3',
    '/dev/ttyUSB4',
    '/dev/ttyACM0',
    '/dev/ttyACM1',
    '/dev/ttyACM2',
    '/dev/ttyACM3',
    '/dev/ttyACM4' 
  )

ports = []

header = """<!DOCTYPE HTML>
<html lang="ja">
 
<head>
  <meta charset="utf-8">
 <title>CO2センサ比較</title>
</head>
 
<body>
  <h1>CO2センサ比較</h1>
  時刻はサーバプログラムがデータを受信した時刻です。データは約1分ごとに自動更新しています。30秒で自動リロードしますが、リロードのタイミングとデータの自動更新のタイミングがぶつかると表示の更新が停止する場合があります。<BR>
  センサーのサンプル数は各1台です。入手経路も入手時期も取り付けの構造もまちまちです。また、気分で改造したりします。
  <HR>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.js"></script>
    <canvas id="Chart1" width="auto" height="auto"></canvas>
    <script>
      var context = document.getElementById('Chart1');
      var Chart1 = new Chart(context, {
        type: 'scatter',
          data: {
            datasets: [
"""
footer = """
              ]},
          options:{
            scales:{
              xAxes: [{
                gridLines: {
                  color: "rgba(255, 0, 0, 0.2)", 
                  zeroLineColor: "black"    },
                type: 'time',
                time: {
                  unit: 'minute',
                  displayFormats: {
                    minute: 'H:mm'
                  },
                },
              }],
              yAxes: [{
                ticks :{
                  userCallback: function(tick) {
                    return tick.toString() + 'ppm'
                  }
                },
                gridLines: {
                  color: "rgba(0, 0, 255, 0.2)",
                  zeroLineColor: "black"
                },
              }]
            },
            animation: false
          }
      });
    </script>
    <script>
    // 30秒に一回リロード
      setTimeout("location.reload(true)",30000);
    </script>
</body>
</html>
"""
prepare = """<!DOCTYPE HTML>
<html lang="ja">
 
<head>
  <meta charset="utf-8">
 <title>CO2センサ比較</title>
</head>
 
<body>
  <h1>準備中です。1分程度で開始します</h1>
  <script>
  // 1分=60秒に一回リロード
    setTimeout("location.reload(true)",60000);
  </script>
</body>
</html>
"""

m1 = re.compile('^\$([A-Z0-9]+)\sCo2:([0-9]+)')

ctbl = (
  'rgba(  0,  0,255,0.8)',
  'rgba(255,  0,  0,0.8)',
  'rgba(  0,128,  0,0.8)',
  'rgba(255,  0,255,0.8)',
  'rgba(  0,128,128,0.8)',
  'rgba(128,128,  0,0.8)',
  'rgba( 64, 64, 64,0.8)'
)

def recieve():
  mkht = time.time()

  for p in ports :
    p.rts = False
    p.reset_input_buffer()
    p.rts = True

  while True:
    for p in ports :
      data = None
      if p.inWaiting() > 0 :
        try :
          data = p.readline().decode().rstrip('\r\n')
        except :
          data = None
          pass
        finally :
          pass
        #print(time.time(),data)
        r1 = m1.match(data)
        if r1 :
          #print(r1.group(1),r1.group(2),time.time())
          sensor = r1.group(1)
          data = float(r1.group(2))
          tstamp = time.time()
        else :
          data = None

      # 配列にデータを追加
      if data is not None :
        if not sensor in hist  :
          hist[sensor] = []
        hist[sensor].append((tstamp,data))
        while len(hist[sensor]) > hlen :
          del(hist[sensor][0])

    # 定期的にHTMLファイルを生成する。
    if time.time() - mkht > 10 :  # 前回ファイル出力から10秒以上経過している場合
      mkht = time.time()
      s = ""
      cidx = 0
      for k in hist.keys():
        s += """
              {
                label: \"""" + k + """\",
                showLine: true ,
                lineTension: 0 ,
                fill: false ,
                borderColor: \"""" + ctbl[cidx] + """\",
                borderWidth: 1,
                pointRadius: 1,
                data :[
"""
        cidx += 1
        for i in hist[k]:
          if mkht - i[0] < 3600*6 :    # 過去20分以内のデータに限り出力する
            s += "{{ x: {:.0f} , y: {} }},".format(i[0]*1000,i[1])
        s = s.rstrip(',')
        s += "]},"
      s = s.rstrip(',')
      with open('index.html',mode='w') as f:
        f.write(header + s + footer)

  for p in ports :
    p.close()

#
if __name__ == "__main__":
  for p in devs :
    if os.path.exists(p) :
      ports.append(serial.Serial(p , baudrate=115200, parity=serial.PARITY_NONE, rtscts=False))
  with open('index.html',mode='w') as f:
    f.write(prepare)
  time.sleep(5)
  recieve()

これをRaspberry Pi 3上で動かして、データ収集しながらHTML生成をさせました。

すべてのセンサを自室内の10cmほど開けてある窓に近いところに置いて比較してみました。

SCD30は変動が大きく、自分がそちらを向くと1mくらいあっても呼気に反応するのか、あるいはちょっとした風向きの変化でCO2濃度が変化する(外気があたる or 室内の空気があたるかが変わる)のか、すぐに数値が上昇します。その他のものは概ね似たような傾向を示しています。

このあと、窓を開けたまま風呂に入って無人の時間がしばらく経過して戻ってきたところです。

やはり、SCD30が極めて敏感に反応しているのですが、400ppmを切ってしまっているので、まだオートキャリブレーションが正常に機能していないのだと思います。(SCD41もまだ稼働時間は24時間以下です)

しばらく様子見するしかなさそうです。