8
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Lチカで始めるテスト自動化

Last updated at Posted at 2020-05-13

##1. はじめに
Lチカといえば初めて作るプログラムの定番ですがテストは人力(LEDの点灯/消灯を目視確認)で行っていることと思います。そこで、スイッチのON/OFFに連動してLEDが点灯/消灯するLチカプログラムとテストベンチを製作し、テストを自動化します。

testbench.png

自動化の対象 自動化の方法
スイッチのON/OFF操作 テスト対象のスイッチから線材を飛ばしてリレーに接続し、テスト治具でリレーを制御する
LEDの点灯/消灯の確認 LEDの電圧をマイコンのA/Dコンバータで測定する
Go/No-Go判定 結果と期待値(基準値)を比較する

以下の動画はArduino治具とオシロスコープをRaspberry Piに接続してテストベンチを組み、Jenkinsでテストを自動化するで製作したテストベンチに組み込んでテストを実行している様子です。

##2. テスト対象
Arduino(UNO R3やLeonardo)のDigital Pin #2に接続されたスイッチを押下すると Digital Pin #12に接続しているLEDが点灯する、というものです。LEDの電流制限抵抗は3.3kΩです。

Lチカボード

ArduinoLedLight.ino
// LED
class Led
{
    public:
        Led(int pin);
        void on();
        void off();
    private:
        int m_led_pin;
};

Led::Led(int pin)
{
    m_led_pin = pin;
    pinMode(m_led_pin, OUTPUT);
    digitalWrite(m_led_pin, LOW);
}

void Led::on(void)
{
    digitalWrite(m_led_pin, HIGH);
}

void Led::off(void)
{
    digitalWrite(m_led_pin, LOW);
}

// SWITCH
class Sw
{
    public:
        Sw(int pin);
        bool isOpen();
    private:
        int m_sw_pin;
};

Sw::Sw(int pin)
{
    m_sw_pin = pin;
    pinMode(m_sw_pin, INPUT_PULLUP);
}

bool Sw::isOpen(void)
{
    if ( digitalRead(m_sw_pin) ) //HIGH:Open, LOW:Close
    {
        return true;
    }
    else
    {
        return false;
    }
}

// initialize
Led led = Led(12);
Sw  sw  = Sw(2);
void setup()
{
    // nothing
}

// logic
void loop()
{
    if( sw.isOpen() )
    {
        led.off();
    }
    else
    {
        led.on();
    }
}

##3. テスト治具
テスト治具はコマンドで操作します。

###3.1 コマンド
UARTの通信速度は115,200[bps]です。

|コマンド|操作内容
|-------+---
|n |リレーをONする
|f |リレーをOFFする
|p |リレーがアサインされているピン番号を表示する
|v |Analog Pin A0に入力された電圧を0[mV]~5,000[mV]の範囲で表示する
|i |テスト治具のモデル名やバージョンを表示する
|h |ヘルプメッセージを表示する

###3.2 プログラム
ピンアサインを以下に示します。

|Pin |接続
|---------------+---
|Digital Pin #12|リレー駆動回路(4章)のGPIO端子
|Analog Pin A0 |テスト対象(2章)のLEDのアノード側

ArduinoLedLightTestBench.ino
// RELAY
class Relay
{
    public:
        Relay(int pin);
        void on();
        void off();
        void pin_number();
    private:
        int m_relay_pin;
};

Relay::Relay(int pin)
{
    m_relay_pin = pin;
    pinMode(m_relay_pin, OUTPUT);
    digitalWrite(m_relay_pin, LOW);
}

void Relay::on(void)
{
    digitalWrite(m_relay_pin, HIGH);
}

void Relay::off(void)
{
    digitalWrite(m_relay_pin, LOW);
}

void Relay::pin_number(void)
{
    Serial.println(m_relay_pin);
}

// ADC
class Adc
{
    public:
      Adc(int pin);
      long get_value(); // 0[mV] - 5,000[mV]
    private:
      int m_adc_pin;
};

Adc::Adc(int pin)
{
    m_adc_pin = pin;
}

long Adc::get_value(void)
{
    int val = analogRead(m_adc_pin);
    return map(val, 0, 1023, 0, 5000);
}

// initialize
Relay relay = Relay(12);
Adc   adc   = Adc(0);

void setup()
{
    Serial.begin(115200);
    //Serial.println("Arduino Test Bench");
}

// logic
void loop()
{
    while(1)
    {
        if(Serial.available())
        {
            char buf;
            buf = Serial.read();
            if(buf == 'n') // oN
            {
                relay.on();
            }
            if(buf == 'f') // oFf
            {
                relay.off();
            }
            if(buf == 'p') // Pin number
            {
                relay.pin_number();
            }
            if(buf == 'v') // get_adc_Value
            {
                long adc_value = 0;
                adc_value = adc.get_value();
                Serial.println(adc_value);
            }
            if(buf == 'i') //Information
            {
                Serial.println("Arduino Test Bench Ver.100");
            }
            if(buf == 'h') // Help
            {
                Serial.println("n : relay oN");
                Serial.println("f : relay oFf");
                Serial.println("p : relay Pin number");
                Serial.println("v : adc a0 Value(0[mV] - 5,000[mV])");
                Serial.println("i : model and version Information");
            }
        }
    }
}

##4. リレー駆動回路
マイコンのGPIOの出力電流はリレーを駆動するには足りないためトランジスタで駆動回路を組みます。

リレー駆動回路

|部品 |型番 |数量|備考
|--------------+------------------------------------------------------------------+----+---
|リレーU1 |G5V-1 DC5 |1 |コイル 5V 30mA
|トランジスタQ1|2SC-1815 |1 |
|抵抗R1 |2.2kΩ |1 |1kΩ~4.7kΩ
|抵抗R2 |10kΩ |1 |
|ダイオードD1 |1S10 |1 |汎用小信号用ダイオード1N4148や汎用整流用ダイオード1N4007など、逆耐電圧が回路電圧の10倍以上かつ順方向電流が負荷電流以上のもの

|回路|接続
|----+---
|VCC |テスト治具(3章)の5V
|GND |テスト治具(3章)のGND
|GPIO|テスト治具(3章)のDigital Pin #12
|a |テスト対象(2章)のDigital Pin #2
|c |テスト対象(2章)のGND

##5. テストベンチの組み立て
重要な注意

  • テスト治具に電源を投入してからテスト対象を操作すること。
  • 電源電圧よりも高い電圧を入力ポートに印加するとマイコンを破損する恐れがあります。
  • AVR ATmega328のRESETピン以外のピンの絶対最大定格電圧は-0.5V~Vcc+0.5Vです1

テスト対象、テスト治具、リレー駆動回路の結線を以下に示します。

  • テスト対象はUSB電源アダプターやモバイルバッテリーで給電します。
  • テスト治具はテストランナー(7章)のPCに接続します。
  • 4章のリレー駆動回路の抵抗R1、R2は、以下の回路図ではそれぞれR2、R3になっています。
  • リレーのON/OFFが分かるように黄色のLEDを追加しています(回路図のLED2、R4)。

led-blink-test-bench_breadboard.png
led-blink-test-bench_circuit.png

  • 実機の実装例(手持ちの都合で片方がArduino Leonardoだったりリレーが941H-2C-5Dになっています)。

test-benchV2.jpg

##6. テスト設計

  • テスト治具のADCにはLED点灯時に約1.8V、消灯時に0Vが印加されますのである基準値(例:1.6Vを超え2.0V未満なら点灯、0.2V未満なら消灯)を決めて点灯・消灯を判定します。
  • 接続しているテスト治具が意図したものかといった確認も行います。
  • 不具合を検出したらそこでテストを中止します。

|手順|確認したいこと |操作 |期待値
|----+----------------------------------------------+-----------------------------+---
|1 |テスト治具のモデル名などが正しいこと |テスト治具にコマンド"i"を送る|コマンドの戻り値が期待通りの文字列であること
|2 |リレーがアサインされているピン番号が正しいこと|テスト治具にコマンド"p"を送る|コマンドの戻り値が12であること
|3 |スイッチを閉じるとLEDが点灯する |テスト治具にコマンド"n"を送る|テスト対象のLEDが点灯すること
| | |テスト治具にコマンド"v"を送る|コマンドの戻り値が1600より大きく2000より小さいこと
|4 |スイッチを開くとLEDが消灯する |テスト治具にコマンド"f"を送る|テスト対象のLEDが消灯すること
| | |テスト治具にコマンド"v"を送る|コマンドの戻り値が200より小さいこと

##7. テストランナーの実装
6章のテストを実行できるようなテストランナーを実装します。Excel VBAでもPythonでも構いませんがここではPythonの例を示します。

###7.1 実行環境
実行環境はWindows10(1909)+Anaconda3 2019.07 (Python 3.6.9 64-bit)です。COMポートはCOM3が割り当てられています。

Anacondaのプロンプト画面でコマンドで実行します。

c:\hoge>python test-runner.py

####7.1.1 Windows10(1909)+WSL Ubuntu 18.04 LTSで動かす
Windows10(1909)+WSL Ubuntu 18.04 LTSで動かすには以下のようにします。

  • COMポートの指定を"COM3"ではなく"/dev/ttyS3"にする
  • teratermなどのソフトであらかじめ"COM3"で通信を行う

原因は分かっていませんが筆者の環境ではあらかじめteratermなどで通信を行うことでコマンド送受信をできるようになりました。

####7.1.2 Raspberry Piにテスト治具(Arduino UNO)を接続し、Raspberry Piで動かす
test-runner.pyの改修箇所は下記2点です。

  • 改行コード→LF
  • COM3→/dev/ttyACM0

###7.2 テストランナーのコマンド

|コマンド |引数 |機能
|-----------+----------------------+-------------------------------------------------
|# |なし |#で始まる行はコメント行とみなす
|sleep |スリープ時間(秒) |コマンド実行の待ち時間を指定する
|open_uart |テスト治具のポート番号|COM3、/dev/ttyS3、/dev/ttyACM0などを指定する
|send |テスト治具のコマンド |テスト治具にコマンドを送信する
|rcvd |なし |テスト治具の応答を受信し変数valにストアする
|eval_str_eq|期待値(文字列) |変数valが期待値の文字列と等しいか評価する
|eval_int_eq|期待値(int型) |変数valが期待値の値(int型)と等しいか評価する
|eval_int_gt|基準値(int型) |変数valが基準値(int型)より大きいか評価する
|eval_int_lt|基準値(int型) |変数valが基準値(int型)より小さいか評価する

###7.3 テストランナー本体
CSV形式でscript.csvに記述されているテストスクリプトを読み、result.csvに実行結果を格納します。テスト治具のポート番号はテストスクリプトで指定します。

test-runner.py
#!/usr/bin/python3

#
# This software includes the work that is distributed in the Apache License 2.0
#

from time import sleep
import serial
import codecs
import csv
import sys

UNINITIALIZED = 0xdeadbeef

def serial_write(h, string):
    if h == UNINITIALIZED:
        print("UART Not Initialized.")
        return False
    else:
        string = string + '\n'
        string = str.encode(string)
        h.write(string)
        return True

def main():
    is_passed = True
    val = str(UNINITIALIZED)
    uart = UNINITIALIZED

    with codecs.open('script.csv', 'r', 'utf-8') as file:
        script = csv.reader(file, delimiter=',', lineterminator='\r\n', quotechar='"')

        with codecs.open('result.csv', 'w', 'utf-8') as file:
            result = csv.writer(file, delimiter=',', lineterminator='\r\n', quotechar='"')

            for cmd in script:
                #print(cmd)

                if "#" in cmd[0]:
                    pass

                elif cmd[0]=="sleep":
                    sleep(float(cmd[1]))
                    cmd.append("OK")

                elif cmd[0]=="open_uart":
                    try:
                        uart = serial.Serial(cmd[1], 115200, timeout=1.0, dsrdtr=1)
                        cmd.append("OK")
                    except:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="send":
                    ret = serial_write(uart, cmd[1])
                    if ret == True:
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="rcvd":
                    try:
                        val = uart.readline().strip().decode('utf-8')
                        cmd.append(val)
                        cmd.append("OK")
                    except:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_str_eq":
                    if str(val) == str(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_int_eq":
                    if int(val) == int(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_int_gt":
                    if int(val) > int(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_int_lt":
                    if int(val) < int(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                else:
                    cmd.append("#")

                print(cmd)
                result.writerow(cmd)

                if is_passed == False:
                    print("FAIL")
                    sys.exit(1)

    if is_passed == True:
        print("PASS")
        sys.exit(0)

main()

###7.4 テストスクリプト
通信開始時にArduino UNOにリセットがかかることがあるためテストスクリプトの冒頭で2秒のsleepを入れています。

script.csv
# wait for if restart Arduino Uno
sleep,2
#
# Open UART
open_uart,COM3
#
# model and version check
send,i
rcvd
eval_str_eq,Arduino Test Bench Ver.100
#
# relay assigned Pin number check
send,p
rcvd
eval_int_eq,12
#
# relay ON
send,n
sleep,1
send,v
rcvd
eval_int_gt,1600
eval_int_lt,2000
#
# relay OFF
send,f
sleep,1
send,v
rcvd
eval_int_lt,200
#
# end

###7.5 テスト実行結果
script.csvと似ていますが2カラム目にrcvdの戻り値が、3カラム目にOKまたはNGが格納されています。

result.csv
# wait for if restart Arduino Uno
sleep,2,OK
#
# Open UART
open_uart,COM3,OK
#
# model and version check
send,i,OK
rcvd,Arduino Test Bench Ver.100,OK
eval_str_eq,Arduino Test Bench Ver.100,OK
#
# relay assigned Pin number check
send,p,OK
rcvd,12,OK
eval_int_eq,12,OK
#
# relay ON
send,n,OK
sleep,1,OK
send,v,OK
rcvd,1788,OK
eval_int_gt,1600,OK
eval_int_lt,2000,OK
#
# relay OFF
send,f,OK
sleep,1,OK
send,v,OK
rcvd,0,OK
eval_int_lt,200,OK
#
# end
result.csv(テスト治具のバージョン不一致)
# wait for if restart Arduino Uno
sleep,2,OK
#
# Open UART
open_uart,COM3,OK
#
# model and version check
send,i,OK
rcvd,Arduino Test Bench Ver.095,OK
eval_str_eq,Arduino Test Bench Ver.100,NG
result.csv(スイッチをONしたのにLEDが点灯していない)
# wait for if restart Arduino Uno
sleep,2,OK
#
# Open UART
open_uart,COM3,OK
#
# model and version check
send,i,OK
rcvd,Arduino Test Bench Ver.100,OK
eval_str_eq,Arduino Test Bench Ver.100,OK
#
# relay assigned Pin number check
send,p,OK
rcvd,12,OK
eval_int_eq,12,OK
#
# relay ON
send,n,OK
sleep,1,OK
send,v,OK
rcvd,0,OK
eval_int_gt,1600,NG

##8. ステップ数
かぞえチャオ!Ver.1.68でステップ数を数えました(ファイル名はかぞえチャオで開けるよう適当に変更しています)。

パス名 総ステップ数 実ステップ数 コメント[%]
ArduinoLedLight.cpp 77 65 5.6
ArduinoLedLightTestBench.cpp 107 92 7.1
test-runner.py 113 86 3.4
全ステップ数 297 243 5.0

##9. おわりに

##10. 参考資料
この記事を作成するにあたって参考にさせていただいた記事です。

##11. 変更履歴
|日付 |変更内容
|----------+----
|2020-05-14|・テスト実行の動画のリンクを追加
|2020-05-16|・7.1.2(Raspberry Piにテスト治具(Arduino UNO)を接続し、Raspberry Piで動かす方法)を追記
・9(おわりに)を変更
|2020-05-17|・テスト実行の動画を差し替え
・Raspberry Pi(テストランナー)にArduino UNO(テスト治具)を接続してWindows10のJenkinsからテストを実行している様子を追記
|2020-05-19|・テストランナーにopen_uartコマンドを追加しテスト治具のポート番号をテストスクリプトで指定するように修正
・テストランナーのライセンスにApache License 2.0を指定
・8(ステップ数)、9(おわりに)を変更
|2020-05-21|・テストランナーの変数名を修正(err→is_passed)
|2020-06-04|・ダイオードの選定方法を補足

  1. ATmega328P Datasheet 28.1 Absolute Maximum Ratings

  2. test-runner.pyの改修箇所を7.1.2に追記しました

8
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?