アルコール検知機を作ってみた

# はじめのはじめ
この記事は趣味で工作を楽しんでいる人が、
- 勉強のために自分のやったことをまとめる
- やったことを共有することで、誰かの役に立といいな

という想いと目的で書いています。


# 目次
[1. はじめに](#1-はじめに)
[2. 構成検討](#2-構成検討)
[3. 設計](#3-設計)
[4. 作製](#4-作製)
[5. おわりに](#5-おわりに)

# 1. はじめに
## 概要
昨今運転中のアルコール規制は厳しくなっていますが、
そういえば呼気中のｱﾙｺｰﾙ濃度を測定する方法が家に無いな～と思ったので、
とりあえず深く考えずに作ってみることにしました。
あとMOSFETでﾊｲｻｲﾄﾞｽｲｯﾁも作ってみたかったのでそれも盛り込むことにしました。
## コンセプト
- なるべく安く作る
- 1ﾎﾞﾀﾝで電源ON/OFFと計測開始を兼ねる回路構成
- 充電式の電池駆動
- Fusion360で3D設計
- PCB基板を作製してみる


# 2. 構成検討
## アルコール検出センサー
呼気中ｱﾙｺｰﾙ検知の方法として、いくつか検出方式がありますが、
2023年現在、市販のｾﾝｻでは半導体式が圧倒的に安いので、半導体式を用いることにします。
原理は下記が詳しい。
赤外吸光式が精度、安定性ともに良さそうですね。
最近の市販品はこの方式なんですかね。

https://www.akm.com/jp/ja/products/co2-sensor/tutorial/types-mechanism/

使ったｾﾝｻはMQ-3です。
SnO2の酸化/還元状態が周辺ｶﾞｽより変化し、抵抗値が変わる現象を用いたｾﾝｻです。

https://www.waveshare.com/wiki/MQ-3_Gas_Sensor

ﾃﾞｰﾀｼｰﾄに記載ありますが、周辺温度により強く影響を受けるので、
暖気時間が24時間以上が推奨とあります。
どの程度で安定するのか、センサ単品でアナログ値を読み取って確認してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/e1965d2e-c19d-87d1-f593-55deba1294ca.png)
記載の通り安定までの暖気は結構かかりますね。（横軸は稼働後の経過sec、縦軸はｱﾅﾛｸﾞ値 1024max）
ｱﾙｺｰﾙｳｪｯﾄﾃｨｯｼｭで検出できることも確認できました。
しらふの呼気でも多少増加する傾向ついでに確認。
検出後の安定には多少時間かかりますが、安定前に再度呼気をかけてもそれなりにピーク値は再現してます。
よって、安定時間はそこまでやらなくても、ピーク値検出はできると判断できそう。
今回はそこまでの精度を求めないので、使用毎に電源をON/OFFする仕様にします。
暖気時間は使いながら考えればヨシ。
ただ当然ですが精度を追った使用方法の際は、常時稼働状態にすべきですね。

校正をするため、ﾃﾞｰﾀｼｰﾄの校正曲線をﾌｨｯﾃｨﾝｸﾞして関数化し、
検出値からガス濃度へ計算できるようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/1279adc9-901d-f7aa-faac-ab1648b39a94.png)
- Air状態のRs/R0が60ﾃﾞｰﾀｼｰﾄより規定されている
- Air状態の入力ｱﾅﾛｸﾞ値を電圧に変換
- ﾛｰﾄﾞ抵抗はﾃﾞｰﾀｼｰﾄより200kΩとあるので、ここからAir状態のRsを計算
- Rs/R0=60からR0値を計算
- 先のﾌｨｯﾃｨﾝｸﾞで導出したRs/R0とガス濃度の関係式に代入してガス濃度を計算

という型で事前にR0計算をして、のちのｺｰﾄﾞに入れ込む形にしました。
温度や湿度依存があるので、温湿度センサとセットにして、
補正係数をかけるようにするとより精度があがりますが、今回はやらない。

## 電源回路
電源には充電式の角型9V電池を用います。
Micro USBで充電ができるタイプです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/b78da50e-0bb0-15a4-e2da-f4b232dc737b.png)

Arduinoは内部ﾚｷﾞｭﾚｰﾀで9V入力のままでOKですが、
MQ-3はﾃﾞｰﾀｼｰﾄより5±0.1Vとあるので、別途ﾚｷﾞｭﾚｰﾀをかませることにします。

ﾀｸﾄｽｲｯﾁをﾎﾞﾀﾝにして、MOSFETで電源ON/OFFを制御しつつ、
そのﾎﾞﾀﾝで測定開始のﾄﾘｶﾞとするようにします。
その部分だけ抜き出した回路図は下記です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/cc3b6c89-54bb-3ac7-1f05-85abc6aa9fb6.png)
pMOSでﾊｲｻｲﾄﾞを止めに行く回路にしてます。
ArduinoのGND側をﾛｰｻｲﾄﾞで止める方法でも構成できそうですが、
ｶｿｰﾄﾞｺﾓﾝのLEDをGPIOに接続した際、OFF状態でもGPIOから電流が漏れてしまうｹｰｽがあったため、
ﾊｲｻｲﾄﾞ側で止める構成が必要と考えました。

ﾊｲｻｲﾄﾞで止める回路構成は下記を参考にさせていただきました。

https://westgate-lab.hatenablog.com/entry/2020/02/01/150000


## 表示器
7segLEDで測定値の表示、3色LEDで測定値が規格通りかどうかの一目可視化を行います。
7segの表示には74HC545のｼﾌﾄﾚｼﾞｽﾀを使いました。
余っていたので。

## 全体構成
構成図と部品類はざっくりこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/53a2d389-dbb8-2778-e31b-87c15056d4df.png)
動作ﾌﾛｰもざっくりこんな感じで動かしてみることにします。
あとは作りながら微修正していく作戦。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/1c98a368-f2a6-2bd2-16a4-5245086911aa.png)

# 3. 設計
## 動作仕様
前述の通り、今回は1ﾎﾞﾀﾝで電源のON/OFF制御と測定開始のﾄﾘｶﾞを兼ねさせる設計をします。

Arduino側は
- ﾎﾞﾀﾝ押している間、電源供給でArduinoが起動
- Arduino起動したら、GPIOで保持するnMOSをON
- ﾎﾞﾀﾝ離しても、GPIOで電源ONを保持
- ﾎﾞﾀﾝ長押しでGPIOで長押し検知、電源ONを保持するnMOSをOFFにして電源遮断
- ﾎﾞﾀﾝ短押しの場合は測定開始
というｼｰｹﾝｽArduinoで作ります。



## 回路図
いつも通りFusion360で回路～筐体設計まで一括でやりました。
回路図は下記です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/d98bb548-02ee-6c73-0c65-502f146cba37.png)

## 配線図
配線図は下記です。
後述しますが、手持ちの素子と同じCADモデルがなかったので、
適当なMOSFET,、レギュレータをライブラリからモデル選択して配置しましたが、
SDGのピン配置などが違ってて、一部で現物合わせが発生してしまいました。
3ピン以上はちゃんとピン配置を確認しないと駄目ですね。今後注意。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/cf128ca5-c57e-e030-adae-6543b64f8489.png)

## 筐体＆3D配置図
こんな感じで配置しました。
筐体は3Dﾌﾟﾘﾝﾀで作りました。材質はPLAです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/03301181-c76c-cc12-000a-d54bafd6cefa.png)
フタも嵌合で作成しようと思いましたが、寸法や形状がうまくできなかったので
とりあえず全面むき出しで。



# 4. 作製
まずﾌﾞﾚｯﾄﾞﾎﾞｰﾄﾞでの動作確認から。
配線ごちゃごちゃしますが、意図どおりの動作シていることを確認。

https://www.youtube.com/watch?v=rIflA6MZcXg&t=1s

ついでに稼働中のｾﾝｻ部温度を確認してみました。
SnO2を活性にするため、抵抗加熱しているのでﾋﾟﾝﾎﾟｲﾝﾄで80℃くらいにはなってます。
指が入らないようにはなってますが、お子さんがいる場合はちょっと注意。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/4ad64463-5ad4-4593-6cd0-d89bc8924034.png)

筐体作製して、各部品実装をしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/f0d4f276-b364-a816-7c17-b5672732b5db.png)

一部素子にｼﾞｬﾝﾌﾟ線かましてますが、CADﾓﾃﾞﾙと実物で異なるものを配置したため、
ﾋﾟﾝ配置を誤って基板を作っちゃいました。
そのために足元にｼﾞｬﾝﾊﾟ線をかましてごまかしてます。
MOSFETとか3端子ﾚｷﾞｭﾚｰﾀとか、意外とﾒｰｶｰによってﾋﾟﾝ配置違うんですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112433/37222a23-f305-5fe1-b9fb-0054067b4522.png)

最終的な動作確認です。
ﾃｨｯｼｭを近づけて0.3mg/Lと、若干低めになっているように感じます。
もう少し校正をイジったほうがよいかもしれないけど、それはおいおい実施するとして、
とりあえず完成。
https://www.youtube.com/watch?v=ETKMnI8H-C4


ソースコードは下記です。

<details><summary><b><font color="#409202">╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╮<br>╏&nbsp;&nbsp;&nbsp;&nbsp;<font color="#409202">ソースコードを表示（折りたたみ）</font>&nbsp;&nbsp;&nbsp;╏<br>╰╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╯</font></b></summary><div>

```php
//Arduino IDEボードが「Arduino Pro or Pro mini」を選択
#include "Arduino.h"

//ピンアサイン
int bot = 2; //入力ボタンのピン
int pow_keep =3; //電源ONキープのピン
int dig1 = 4;       // DIG1割り当て
int dig2 = 5;       // DIG2割り当て
int dig3 = 6;       // DIG3割り当て
int dsPin = 7;     // (14) DS [SER] on 74HC595
int srclkPin = 8;   // (11)  SH_CP [SRCLK] on 74HC595
int rclkPin = 9;   // (12) ST_CP [RCLK] on 74HC595
int LED_Red=10; //赤LEDのピン
int LED_Green=11; //緑LEDのピン
int LED_Blue=12; //青LEDのピン
int MQ_analog= A0; //MQ-3のアナログ出力ピン

//変数定義
unsigned long pre_time = 0; //前回の時間
unsigned long curr_time = 0; //現在の時間
unsigned long off_time= 30000; //自動オフまでの時間
unsigned long warm_up_time= 10000; //校正時間
float raw_val = 0.0; //MQ-3のアナログ生値

//MQ用の値
float RL=200000;//センサの可変抵抗値
float R0=0.0;//air状態でのセンサ抵抗 起動時に決めるが、だいたい6.4kΩ
float V0=0.0;//air状態でのセンサ電圧 起動時に決めるが、だいたい1.7V
float v0_defoult=1.2;//事前測定したデフォルト値
float max_vol=5.0;//Aruinoの電圧max値
float Vs=0.0;//計測したセンサの電圧値
float air_val=60;//air状態でのセンサ値
float alc_val=0.0;//alcohol状態でのセンサ値

int warm_up_cnt=500;//airで校正する暖気時間


int disp_seg_delay = 5; //7segの点滅時間
int num_1 = 0; //1桁目の位の数字
int num_2 = 0; //2桁目の位の数字
int num_3 = 0; //3桁目の位の数字


//シフトレジスタ用定義
// seven_ledsをbyte型として定義
// 配列にDisplay表示用のデータ0~9と全消灯を設定
// 1 = LED OFF, 0 = LED ON

//小数点OFF
byte seven_leds[12] =       { B11111100,  // 0
                              B01100000,  // 1
                              B11011010,  // 2
                              B11110010,  // 3
                              B01100110,  // 4
                              B10110110,  // 5
                              B10111110,  // 6
                              B11100100,  // 7
                              B11111110,  // 8
                              B11110110,  // 9
                              B00000001,  // .
                              B00000000,  // OFF
                             };
//小数点ON
byte seven_leds_dp[12] =    { B11111101,  // 0
                              B01100001,  // 1
                              B11011011,  // 2
                              B11110011,  // 3
                              B01100111,  // 4
                              B10110111,  // 5
                              B10111111,  // 6
                              B11100101,  // 7
                              B11111111,  // 8
                              B11110111,  // 9
                              B00000001,  // .
                              B00000000,  // OFF
                             };



//シフトレジスタ部分を関数化
void funcShiftReg(int x)
{
  digitalWrite(rclkPin, LOW);                     //送信中のRCLKをLowにする
  shiftOut(dsPin, srclkPin, LSBFIRST, seven_leds[x]); //シフト演算を使って点灯するLEDを選択
  digitalWrite(rclkPin, HIGH);                        //送信終了後RCLKをHighにする
}

void funcShiftReg_dp(int x)
{
  digitalWrite(rclkPin, LOW);                     //送信中のRCLKをLowにする
  shiftOut(dsPin, srclkPin, LSBFIRST, seven_leds_dp[x]); //シフト演算を使って点灯するLEDを選択
  digitalWrite(rclkPin, HIGH);                        //送信終了後RCLKをHighにする
}

//センサ初期化
void initial_MQ(){
    //センサの初期化
    //センサの電圧を計測して平均化
    Serial.println("start initializing sensor");
    for(int i=0;i<warm_up_cnt;i++){
        raw_val += analogRead(MQ_analog);
        delay(10);
        if(i%10==0){
            Serial.println("raw_val:"+String(raw_val/(i+1)));
        }
    }
    raw_val /= warm_up_cnt;
    V0=raw_val*max_vol/1024.0;
    R0 = RL*(max_vol/V0-1);

    Serial.println("raw_val = "+String(raw_val)+" R0 = "+String(R0)+" V0 = "+String(V0));
}

//ボタン入力のチェック　通常は1、長押しは2を返す
int push_bottun_check(){
    unsigned long cnt_0 = millis();
    unsigned long cnt_1 = millis();
    delay(200);
    if(digitalRead(bot) == HIGH){
        // delay(300);//チャタリング防止
        // if(digitalRead(bot) == HIGH){//0.3sec後も押されてるか
            while(digitalRead(bot) == HIGH){
                cnt_1 = millis();
                if(cnt_1 - cnt_0 > 1500){//1sec以上押されているか確認
                    return 2;
                }
            }
            return 1;//1sec未満の入力は1を返す
        // }
    }
    Serial.println("return 0");
    return 0;
}

int push_bottun_check2(){
    unsigned long cnt_0 = millis();
    unsigned long cnt_1 = millis();
    delay(200);
    if(digitalRead(bot) == HIGH){
        // delay(300);//チャタリング防止
        // if(digitalRead(bot) == HIGH){//0.3sec後も押されてるか
            while(digitalRead(bot) == HIGH){
                cnt_1 = millis();
                if(cnt_1 - cnt_0 > 1500){//1sec以上押されているか確認
                    return 2;
                }
            }
    }
    return 1;
}

void LED_Green_ON(){
    digitalWrite(LED_Blue, HIGH);
    digitalWrite(LED_Green, LOW);
    digitalWrite(LED_Red, HIGH);
}

void LED_Red_ON(){
    digitalWrite(LED_Blue, HIGH);
    digitalWrite(LED_Green, HIGH);
    digitalWrite(LED_Red, LOW);
}

void LED_Blue_ON(){
    digitalWrite(LED_Blue, LOW);
    digitalWrite(LED_Green, HIGH);
    digitalWrite(LED_Red, HIGH);
}

void LED_Yellow_ON(){
    digitalWrite(LED_Blue, HIGH);
    digitalWrite(LED_Green, LOW);
    digitalWrite(LED_Red, LOW);
}

void LED_OFF(){
    digitalWrite(LED_Blue, HIGH);
    digitalWrite(LED_Green, HIGH);
    digitalWrite(LED_Red, HIGH);
}

// void intertupt_bot(){
//     Serial.println("interrupt");
//     disp_test();
// }

void disp_num(){
    Serial.println("start disp_num");
    if(alc_val<0.05){
        LED_Green_ON();
    }
    else if(alc_val<=0.05 && alc_val<0.1){
        LED_Yellow_ON();
    }
    else{
        LED_Red_ON();
    }

    pre_time = millis();
    while(millis()-pre_time<10000){
        //1桁目表示
        digitalWrite(dig1, LOW); 
        digitalWrite(dig2, HIGH);
        digitalWrite(dig3, HIGH);     
        funcShiftReg(num_3);
        delay(disp_seg_delay);
        //2桁目表示
        digitalWrite(dig1, HIGH);
        digitalWrite(dig2, LOW);
        digitalWrite(dig3, HIGH);
        funcShiftReg(num_2);
        delay(disp_seg_delay);
        //3桁目表示
        digitalWrite(dig1,HIGH);
        digitalWrite(dig2, HIGH);
        digitalWrite(dig3, LOW);
        funcShiftReg_dp(num_1);
        delay(disp_seg_delay);
        if(digitalRead(bot) == HIGH){
            Serial.println("break");
            break;
        }
    }
    digitalWrite(dig1,HIGH);
    digitalWrite(dig2, HIGH);
    digitalWrite(dig3, HIGH);     
}


void get_val(){
    Serial.println("start get_val");
    V0=v0_defoult;//初期設定使わず固定値にする。無効時はコメントアウト
    float sum = 0.0;
    float val = 0.0;
    float tmp_val = 0.0;
    alc_val = 0.0;
    pre_time = millis();
    Serial.println("millis-pretime:"+String(millis()-pre_time));
    while(millis()-pre_time<10000){
        if(digitalRead(bot)==HIGH){
            Serial.println("break");
            break;
        }
        sum=0.0;
        val=0.0;
        tmp_val=0.0;
        int t=millis()-pre_time;
        if((t/1000)%2==0){//0-999sec, 2000-2999secまで
            LED_Blue_ON();
        }
        else{//1000-1999sec, 3000-3999secまで
            LED_OFF();
        }
        for(int i = 0; i < 10; i++){
            val = analogRead(A0);
            sum = sum+val;
            delay(10);
        }
        raw_val = sum/10;
        Vs = (raw_val/1024)*5;
        tmp_val = pow(air_val*(V0/(max_vol - V0))*(max_vol-Vs)/Vs,-1.5);
        alc_val=max(alc_val,tmp_val);
    }
    Serial.println("Rs/R0:"+String(air_val*(V0/(max_vol - V0))*(max_vol-Vs)/Vs));
    Serial.println("raw_val:"+String(raw_val)+" Vs:"+String(Vs)+" alc_val:"+String(alc_val));    
    //表示の文字を計算
    if(alc_val<0.01){
        num_1 = 0;
        num_2 = 0;
        num_3 = 0;
    }
    else if(0.01<=alc_val && alc_val<1){
        num_1 = 0;
        num_2 = int(alc_val*100)/10;
        num_3 = int(alc_val*100)%10;

    }
    else{
        num_1 = 1;
        num_2 = 0;
        num_3 = 0;
    }
    Serial.println("num_1:"+String(num_1)+" num_2:"+String(num_2)+" num_3:"+String(num_3));
}


void setup() {
    // put your setup code here, to run once:
    Serial.begin(9600);
    pinMode(bot, INPUT);
    pinMode(pow_keep, OUTPUT);
    pinMode(dig1, OUTPUT);
    pinMode(dig2, OUTPUT);
    pinMode(dig3, OUTPUT);
    pinMode(dsPin, OUTPUT);
    pinMode(srclkPin, OUTPUT);
    pinMode(rclkPin, OUTPUT);
    pinMode(LED_Blue, OUTPUT);
    pinMode(LED_Green, OUTPUT);
    pinMode(LED_Red, OUTPUT);
    //割り込みピンの設定
    // attachInterrupt(digitalPinToInterrupt(bot),intertupt_bot, CHANGE);

    digitalWrite(pow_keep, HIGH);//これで電源KEEP
    digitalWrite(dig1,HIGH);
    digitalWrite(dig2,HIGH);
    digitalWrite(dig3,HIGH); 
    LED_OFF();


    // //MQ-3の暖気待ち用　固定値使わないとき用
    // pre_time = millis();
    // while(millis() - pre_time < warm_up_time){
    //     LED_Blue_ON();
    //     digitalWrite(dig1, LOW); 
    //     digitalWrite(dig2, HIGH);
    //     digitalWrite(dig3, HIGH);          
    //     funcShiftReg(1);    
    //     delay(300);
    //     LED_Green_ON();
    //     digitalWrite(dig1, HIGH);
    //     digitalWrite(dig2, LOW);
    //     digitalWrite(dig3, HIGH);
    //     funcShiftReg(2);    
    //     delay(300);
    //     LED_Red_ON();
    //     digitalWrite(dig1,HIGH);
    //     digitalWrite(dig2, HIGH);
    //     digitalWrite(dig3, LOW);
    //     funcShiftReg_dp(3);    
    //     delay(300);
    //     LED_OFF();
    //     delay(300);
    // }
    // digitalWrite(dig1,HIGH);
    // digitalWrite(dig2, HIGH);
    // digitalWrite(dig3, HIGH);
    // //MQ-3の初期設定
    // LED_Red_ON();
    // initial_MQ();
    // LED_OFF();
    
    //ボタン押しっぱなしで起動時の自動OFFを止めるためのループ
    while(digitalRead(bot)==HIGH){
        LED_Yellow_ON();
    }

    V0=v0_defoult;//初期設定使わず固定値にする。無効時はコメントアウト
    Serial.println("setup finished");

}

void loop() {
    LED_Blue_ON();
    if(digitalRead(bot)==HIGH){
        Serial.println("ボタンが押されました。ボタン入力のチェック");
        int bot_state=push_bottun_check2();
        if(bot_state==1){
            Serial.println("ボタンが押されました。");
            // LED_Red_ON();
            // disp_test();
            get_val();
            disp_num();
            pre_time = millis();
        }
        else if (bot_state==2){
            Serial.println("ボタンが長押しされました。");
            // LED_Yellow_ON();
            delay(5000);
            digitalWrite(pow_keep, LOW);
        }
        else{
            Serial.println("入力検出ミス");
        }
    }
    curr_time = millis();
    if(curr_time - pre_time > off_time){
        Serial.println("5分経過しました。5sec後に自動オフします");
        pre_time=millis();
        while(millis()-pre_time<5000){
            int t=millis()-pre_time;
            if(t/500%2==0){
                LED_Yellow_ON();
            }
            else{
                LED_OFF();
            }
        }
        delay(5000);
        digitalWrite(pow_keep, LOW);
    }
 
}
```
</div></details>


# 5. 終わりに
ｱﾙｺｰﾙﾁｪｯｶｰを作ってみようと思いたち、ひとまずそれっぽいものが完成できました。
また、ﾊｲｻｲﾄﾞｽｲｯﾁも組み込むことができました。
ただ3Dﾌﾟﾘﾝﾀでのはめあいはうまくできなかったので、おいおい何か別のｸﾗﾌﾄでﾘﾍﾞﾝｼﾞしようかな。

ここまで見ていただいてありがとうございました。