センサー(MQ-3)を使ってラズパイからアルコールを計測してみた
Contents
アルコールセンサー
アルコールセンサー(MQ-3)をRaspberry Pi 3 B+に接続してアルコールをPythonのプログラムで検出してみた時の備忘録。
アルコールセンサー(MQ-3)は単体でも購入する事が出来るが自分は色々なセンサーを試してみたかったのと割安だった事もありMQシリーズのセンサーモジュールセットを購入した。
外観とコネクタ
アルコールセンサーの外観とコネクタは以下の通り。
コネクタは正面左から、
- Vcc(DC5V)
- GND
- Digital Out(デジタル出力)
- Analog Out(アナログ出力)
の4本があり出力信号はデジタル出力とアナログ出力の2種類がある。
デジタルはアルコールを検知したか/してないか(LOW or HIGH)の2種類の検知、アナログの方は電圧に応じて検出量をppm(mg/L)で検出することができる。
今回はデジタルとアナログ両方についての回路図とプログラミングについての記事としている。
裏面は電源投入時に点灯するLED(赤)とアルコールを検知した際に点灯するLED(緑)が2つある。
また100KΩから470 KΩまで調整可能な半固定抵抗(可変抵抗)もあるが最初の設定値の200kΩから変更していない。
スペック
主なスペックは以下の通り。
電圧 | 5V |
標準計測環境 | 温度:20℃±2℃ 湿度:65%±5% |
予熱時間 | データシートによると予熱時間(Preheat time)は24時間以上(Over 24 hour)となっていた しかし”20秒以上”となっている資料もあり、デジタルでアルコールを検知(検知/非検知)するだけであれば20秒も置けばアルコールに反応した いずれにしても内部のコイルで温めてガスを検知する仕組みなので通電直後は正しく動作しないと思われる |
感度特性曲線 | 感度特性曲線はアナログコネクタの電圧からRs/Roを算出してppm(mg/L)を計算する時に使用する この表の見方については後述する データシートより
|
回路図(デジタル)
アルコールを検知(/非検知)だけであればDo(デジタル)をGPIOに接続すれば検知できるのでまずは簡単なデジタル出力から試してみる。
Doutの出力は5Vなので半固定抵抗で3.3V以下までレベル変換してGPIO4に接続している。
プログラム(デジタル)
プログラムは下記の通り。
ソースコード
# -*- coding: utf-8 -*-
"""
Created on Mon Sep 7 21:43:29 2020
・MQ-3でアルコールの検知
・Dout LOW:アルコール検知
@author: Souichiou Kikuchi
"""
import time
import RPi.GPIO as GPIO
MQ3 = 4 # MQ-3接続GPIO番号
GPIO.setwarnings(False) # GPIO.cleanup()をしなかった時のメッセージを非表示にする
GPIO.setmode(GPIO.BCM) # ピンをGPIOの番号で指定
GPIO.setup(MQ3, GPIO.IN)
if __name__ == '__main__':
try:
print('--- program start ---')
while True:
val = GPIO.input(MQ3)
if val == GPIO.LOW:
print('アルコールを検知しました')
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
GPIO.cleanup()
print('--- program end ---')
補足説明
whileループを1秒間隔で回しながらアルコールを検知するとGPIO4がLOWになるのでその際にprintしている。
プログラムはCtrl+cで終了するのだが、その際にGPIOをクリーンアップしている。
実行結果
検証方法
検証方法は料理用日本酒をティッシュに浸してセンサーの前に持っていって反応させる事で検証した。
アルコール用LED(緑)が反応して点灯している。
実行結果
プログラムをRaspberry Pi 3 B+にコピーしてThonnyからの実行結果。
アルコールを検知すると「アルコールを検知しました」とコンソールに表示される。
Dout(デジタル)については以上で終了。
回路図(アナログ)
続いてAout(アナログ)についての回路図。
Raspberry Pi 3 B+ではアナログ値はそのままでは扱えないのでADコンバーター(MCP3208)でデジタル値に変換して電圧を取得する。
事前準備
ADコンバーターからの信号をSPI通信で受信するのでRaspberry Piの設定のインターフェースタブでspiを有効化する。
またPythonのspidevライブラリーを使ったほうがプログラミングが簡単なので下記のコマンドでspidevをインストールする。
sudo apt-get install python-spidev
SPIの有効化とspidevのインストール手順の画面ショットについてはこちらの記事を参照して欲しい。
感度特性曲線
アナログ出力は検出量(ppm※)に応じて電圧を出力するのだがこの際に検出量の算出に前述の感度特性曲線を使用する。
※尚、ppmは百万分の1なのでmg/Lとなる。
データシートより
上記のグラフで見るべきポイントは以下の通り。
- グラフが線形(直線)に見えるのだが0.1から始まっているのと目盛りが等間隔でない
- 複数の成分を検出できる(出来てしまう)
- Air(無検出)の際のRs/Roの値が60(Rs/Roについては後述)
関数の近似値
まずはアルコールの数値を等間隔のグラフにプロットし直してみると下記のような対数関数の様なグラフであることが分かる。
当初は始点と終点の2点間の座標から勾配を計算して10を底とする対数で近似してみたのだが始点と終点に近い座標は程よい値が計算されるのだが中間地点付近ではそれなりにズレてしまう事が分かった。
細かい座標を複数プロットして対数関数で近似しても良いのだが、複数箇所プロットするなら”直線”として近似しても大して変わらない値を取得できる上に計算も簡単なので1次関数(直線)として近似することにした。
細かい座標を複数とって2点間の直線(1次関数)として近似する。
複数の成分
グラフにはアルコールの他にベンジン、ヘキサンなども載っている。
つまり「ベンジンや、ヘキサンも検出されてしまう」という事を意味している。
MQシリーズのセンサーを選択する際は検出したくない成分にも注意を払う必要がある。
Airの意味
Air(空気なので無検出)の際の上限が60になっている。
Rs/Roは率なので通常はRs÷Ro×100で0~100%で表すのだが上限が60なのでRs÷Ro×60で計算する事でRs/Roの最大値が60付近になるようにする。
Rs/Roとは
Rsはセンサーからの検出値。
Roはアルコール類が無検出の時(Airの状態)のベースとなる検出値でプログラムを起動した際に付近にアルコール類を置かない状態で標準(ベース)となるRoの値を算出している。
このベースとなる値(Ro)とセンサーからの検出値(Rs)との率を比較して検出量を算出する。
つまり、
現在のセンサーの検出値÷起動時の無検出の状態の検出値×60
でRs/Roを算出している。
またMQ-3のAoutからの電圧(アナログ値)はADコンバーター(MCP3208)を経由して0~4095の値でプログラムでは取得される。
プログラム(アナログ)
プログラムは以下の通り。
ソースコード
# -*- coding: utf-8 -*-
"""
Created on Mon Sep 7 21:43:29 2020
・MQ-3によるアルコール測定
・対数による関数の近似では無く線形で近似
@author: Souichirou Kikuchi
"""
import spidev
import time
CHN = 0 # ADコンバーター接続チャンネル
V_REF = 5.0 # input Voltage
spi = spidev.SpiDev()
spi.open(0, 0) # 0:SPI0、0:CE0
spi.max_speed_hz = 1000000 # 1MHz SPIのバージョンアップによりこの指定をしないと動かない
def get_adc_data(channel): # ADコンバーターから0~4095の値で電圧を取得する
dout = spi.xfer2([((0b1000+CHN)>>2)+0b100,((0b1000+CHN)&0b0011)<<6,0]) # Din(RasPi→MCP3208)を指定
bit12 = ((dout[1]&0b1111) << 8) + dout[2] # Dout(MCP3208→RasPi)から12ビットを取り出す
return float(bit12) # 0~4095
class MQ3Class(): # MQ-3のクラス
RO_CLEAN_AIR_FACTOR = 60 # 空気がクリーンな状態での最大値(MQ-3データシートより)
CALIBARAION_SAMPLE_TIMES = 50 # 初期調整時の繰り返し回数
CALIBRATION_SAMPLE_INTERVAL = 500 # 初期調整時の読み取り間隔(ミリ秒)
READ_SAMPLE_INTERVAL = 50 # データ取得時の繰り返し回数
READ_SAMPLE_TIMES = 5 # データ取得時の間隔(ミリ秒)
RL_VALUE = 200 # データシート上のRLの抵抗値
ALC = 0 # アルコール
BNZ = 1 # ベンジン
def __init__(self): # コンストラクタ
print('初期調整中...')
self.Ro = self.read_data(self.CALIBARAION_SAMPLE_TIMES, self.CALIBRATION_SAMPLE_INTERVAL)
print('初期調整終了...\n')
print("Ro=%f " % self.Ro)
def get_ppm(self): # ppm(mg/L)を取得するメソッド
val = {}
read = self.read_data(self.READ_SAMPLE_INTERVAL, self.READ_SAMPLE_TIMES)
# 割合を求めるのに100を乗算せずにクリーンな状態での最大値(60)を乗算している
val["ALC"] = self.calculation_ppm(read/self.Ro*self.RO_CLEAN_AIR_FACTOR, self.ALC) # Rs/Roよりアルコールのppmを計算する
val["BNZ"] = self.calculation_ppm(read/self.Ro*self.RO_CLEAN_AIR_FACTOR, self.BNZ) # Rs/Roよりベンジンのppmを計算する
return val
def read_data(self, sample_interval, sample_times): # データを読み取る
val = 0.0
for i in range(sample_times): # 複数回読み込んで平均を算出して誤差を少なくする
val += self.resistance_calculation(get_adc_data(CHN)) # ADコンバーターからの値を合算する
time.sleep(sample_interval / 1000) # 間隔を空ける
val = val/sample_times # 平均を算出する
return val
def resistance_calculation(self, raw_adc): # 抵抗値の計算 raw_adc:0~4095
return float(self.RL_VALUE*(4095.0-raw_adc)/float(raw_adc));
def calculation_ppm(self, rs_ro_ratio, gas_id): # Rs/Roよりppmを計算する
alc_rate = [ # アルコール変換表
[1.7,2.3,-0.2,0.22], # From Rs/Ro, To Rs/Ro, 傾き, 基本となるppm
[1,1.7,-0.27143,0.41],
[0.55,1,-1.53333,1.1],
[0.4,0.55,-4,1.7],
[0.29,0.4,-7.27273,2.5],
[0.2,0.29,-16.66667,4],
[0.17,0.2,-70,6.1],
[0.14,0.17,-63.33333,8],
[0.12,0.14,-100,10],
[0.1,0.12,-4450,99]
]
bnz_rate = [ # ベンジン変換表
[3.2,4,-0.15,0.22],
[2.7,3.2,-0.38,0.41],
[1.8,2.7,-0.76667,1.1],
[1.5,1.8,-2,1.7],
[1.3,1.5,-4,2.5],
[1.1,1.3,-7.5,4],
[0.9,1.1,-10.5,6.1],
[0.84,0.9,-31.66667,8],
[0.8,0.84,-50,10],
[0.1,0.8,-127.14286,99]
]
if ( gas_id == self.ALC ):
__graph = alc_rate
elif ( gas_id == self.BNZ):
__graph = bnz_rate
if rs_ro_ratio > __graph[0][1]: # 指定した率の範囲外の時
ppm = 0
elif rs_ro_ratio < __graph[len(__graph)-1][0]: # 最小値より小さい時
ppm = 999
else:
for i in range(len(__graph)):
if __graph[i][0] <= rs_ro_ratio and rs_ro_ratio < __graph[i][1]: # FromとToの範囲内のrateを検索する
bs_rasio = __graph[i][0] # 基本rate
grad = __graph[i][2] # 傾き
ppm_base = __graph[i][3] # 基本のppm
break
ppm = ppm_base +((rs_ro_ratio - bs_rasio)*grad) # ppmを計算する
return ppm
if __name__ == '__main__':
try:
print('--- program start ---')
mq = MQ3Class();
while True:
perc = mq.get_ppm()
print('アルコール: {0} ppm'.format(perc['ALC']))
print('ベンジン: {0} ppm'.format(perc['BNZ']))
time.sleep(3)
except KeyboardInterrupt:
pass
finally:
spi.close()
print('--- program end ---')
補足説明
import
11行目からのimportで必要なモジュールを読み込んでいる。
spidevはPythonでSPIデバイスを扱う為のモジュール。
定数
CHNはADコンバーターの接続したチャンネル番号。
一番左端に接続したので0を指定してる。
V_REFはMQ-3へのVcc(電圧)で5.0Vを指定してる。
get_adc_data
MQ-3のアナログ出力→ADコンバーターの0チャンネル→0~4095の数値に変換してRaspberry PiのSPIデバイスで取得となっている。
MCP3208に信号を送って(Din)、返ってきた値(Dout)を12ビット(0から4095)で取得しているのだが詳細は以前のこちらの記事を参照して欲しい。
MQ3Class
MQ-3のクラス。
get_ppmメソッドでアルコールとベンジンのppmを計算する。
クラスの定数
28~35行目はMQ3Classで使用している定数の定義。
それぞれコメントを入れているが1回の計測では誤差がでる可能性が高いので複数回MQ-3からセンサー値を取得して平均値から計算するようなロジックになっているので取得回数や間隔(ミリ秒)などを指定している。
init
コンストラクタ(初期処理)。
108行目のmq = MQ3Class();でインスタンスを作成した際に呼ばれる。
0.5秒間隔で50回センサー値を取得して平均からRoを算出している。
尚、この処理の際にはセンサーにアルコール類を近づけずにクリーンな状態での測定値を計算する必要がある。
get_ppm
アルコールとベンジンのppmを計算するメソッド。
read_dataで現在のセンサー値を取得(Rs)してcalculation_ppmメソッドを呼び出してRsとRoの「比率からそれぞれの成分のppmを算出する。
read_data
sample_intervalで指定された間隔(ミリ秒)でsample_times指定された回数分だけget_adc_dataでADコンバーターから0~4095の値でセンサー値を取得する。
calculation_ppm
入力パラメータのrs_ro_ratio(RsとRoの比率)からアルコールとベンジンのppm(mg/L)を計算している。
リストはコメントにもあるが、From Rs/Ro、To Rs/Ro、傾き、基本となるppmの順で作成されている。
最初にrs_ro_ratioがFrom、Toの範囲内に入っているリスト内の行を探す。
※範囲内に無い場合は0、999を返す
例えばアルコールの場合でrs_ro_ratioが0.42の場合、0.4~0.55の範囲内なので4行目の
[0.4,0.55,-4,1.7]
がヒットする。
3つ目の項目の傾きの-4の根拠はppmが1.1(Rs/Roが0.55の時のppm)から1.7(Rs/Roが0.4の時のppm)へ0.6増加する間にrs_ro_ratioが0.55から0.4へと0.15減少しているので0.6÷-0.15=-4としている。
後は一次方程式で該当のRs/Roに対応するppmを算出している。
main
try~except~finallyはエラー処理。
whileで3秒間隔でループをしながらget_ppmメソッドでアルコールとベンジンの検出結果をprint文でコンソールに表示している。
実行結果(アナログ)
プログラムをRaspberry Pi 3 B+にコピーしてThonnyで実行した時の実行結果。
センサーに料理用日本酒を浸したティッシュを近づけるとアルコール及びベンジンの検出量をprint文でコンソールに表示している。
以上で今回の記事を終了とする。
この記事が何処かで誰かの役に立つことを願っている。
尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。
初めまして、kunkunと申します。
いつも拝見させて頂いています。ありがとうございます。
ラズパイ及びpyhtonの勉強を初めて1ヶ月程度で、不躾な質問で申し訳ないのですが、質問させて下さい。
センサー(MQ-3)を使ってラズパイからアルコールを計測してみたの記事で、
アナログの実行結果がどうしてもZeroDivisionError: float division by zeroとなり、アルコール及びベンジンの検出量がprintされません。
記載頂いている内容以外に必要なことはありますでしょうか?
※回路は見様見真似で同じものを作成し、プログラムはコピペさせて頂きました
よろしくお願いいたします。
kunkun さん、こんにちは。
“ZeroDivisionError: float division by zero” はいわゆるゼロ割エラーで何かの数字をゼロで割ってしまう事で発生するエラーです。
割り算をやっている所は何箇所かありますが、仮に
だとすると self.Ro がゼロなのかも知れません。
初期調整終了… 表示の後で Roを表示していますがゼロではありませんか?
仮にself.Ro がゼロだとするとread_data関数の値がおかしい事になります。
ADコンバーターから正しい値が出力されていないのだと思います。
接続が間違っている、接触不良、壊れている等の理由が考えられますが、テスターをお持ちでしたら電圧を測ってみることで原因を絞ることが出来ます。
souichirouさん
さっそくご返信頂いてありがとうございました。
ADコンバーターの接触が悪かったようで、繋ぎ直してみたところ、エラーなく動きました。
ありがとうございます。
本ページ記載の内容を応用させて頂いて、
来年からのアルコールチェック義務化に向けて
アルコールチェックの記録・保存をスプレッドシートで行う
というのをやってみようと考えています。
大変ためになりました。今後も更新を楽しみにしています。