LoginSignup
0
0

More than 1 year has passed since last update.

Lチカで始めるテスト自動化(22)DACをコマンドで制御してLチカする

Last updated at Posted at 2021-10-13

1. はじめに

スタティック点灯1やダイナミック点灯2に続き、今回はDAC(D/A Converter)をコマンドで制御してLチカを試します。

DAC回路

2. LEDの点灯をDACで制御する原理

DACの出力に抵抗Rを介してLEDを接続します。

回路図

DACの出力電圧$V_{DAC}$がLEDの順方向電圧$V_f$よりも大きくなると以下の式で表される電流$I$がLEDに流れます。

\begin{align}
I &= \frac{V_r}{R} \\
&= \frac{V_{DAC}-V_f}{R} \quad{(ただしV_{DAC}>V_f)}
\end{align}

また、抵抗Rの消費電力$P_r$は以下で示されます。

\begin{align}
P_r &= V_r \times I \\
&= V_r \times \frac{V_r}{R} \\
&= \frac{V_r^2}{R} \\
&= \frac{(V_{DAC}-V_f)^2}{R} \quad{(ただしV_{DAC}>V_f)}
\end{align}

$V_f$および$R$を一定とみなし、$V_{DAC}>V_f$とすると、$V_{DAC}$が大きくなるほどLEDに流れる電流$I$も大きくなり明るく光ります。

2.1 電流制限

本稿で使用するLTC1660CNの出力電流はチャンネル1つあたり最大5mAです3。一例として$V_{DAC}$の最大値を5[V]、使用するLEDの$V_f$が1.8[V]4の場合、出力電流が5[mA]を超えないよう640[Ω]より大きな値の抵抗Rを接続してください(一般に入手可能なE12系列やE24系列の抵抗に640[Ω]はないため680[Ω]以上のものを選定します)。抵抗Rの消費電力は16[mW]程度のため1/4[W]や1/8[W]のカーボン抵抗で構いません。

3. LTC1660CN

ArduinoにはDUEというDACを内蔵したモデルがあるのですが、手持ちのArduino LeonardoはDACを内蔵していないため秋月電子通商で販売されている8ch 10bitのLTC1660CNを使用しました。データシートよりブロックダイアグラムを引用します。

LTC1660CN Block Diagram

3.1 制御方法

LTC1660CNはSPI(Serial Peripheral Interface)で制御します。データは16bit、MSB Firstで、クロックの立上りエッジでデータが読み込まれます。

LTC1660CN Register Loading Sequence

3.2 アドレス/コントロール

LTC1660CNは4bitのアドレス空間があります。0b0001~0b1000がDAC A~DAC Hに対応するほか、0b1110でスリープへ、0b1111ですべてのDACチャンネルに同じ設定を行います。

LTC1660CN Address/Control Functions

4. Arduiono Leonardoとの接続

Arduino LeonardoとLTC1660CNの接続を以下に示します。ArduinoのSPIの詳細はSPI libraryをご参照ください。

Arduino Leonardo LTC1660CN
5V VCC, REF, CLR
GND GND
SCK(ICSP-3) SCK
MOSI(ICSP-4) DIN
Digital #2 1つめのLTC1660CNのCS/LD
Digital #3 2つめのLTC1660CNのCS/LD

ICSP端子は以下の写真をご参考ください。
icsp.jpg

5. コマンド

Arduinoのファームウェアに実装するコマンド仕様と実行例を以下に示します。

コマンド 引数 機能
dacset <id> <ch> <value> idで指定したDACの所定のチャンネルに値valueを設定する
dacget <id> idで指定したDACに設定した値を8チャンネル分表示する(区切り文字は半角スペース)
  • idは0から順に振ります
  • chは1~8、15で指定します。1~8がそれぞれDAC A~DAC Hに対応します。15ですべてのDACチャンネルに同じ設定を行います。
  • valueは0以上1023以下の整数で指定します。
dacset 0 1 512       ← id=0のDACのDAC Aに値512を設定する
OK                   ← コマンドのレスポンス
dacget 0             ← id=0のDACに設定した値を表示する
512 0 0 0 0 0 0 0    ← 8チャンネル分の値が返ってくる
OK                   ← コマンドのレスポンス

6. ソース

Arduino Leonardoのソースを以下に示します。

ArduinoLTC1660CN.ino
/***********************************************************************
 * Arduino LTC1660CN
 * 2021-10-10 by ka's
 ***********************************************************************/
/***********************************************************************
 * Copyright 2020 ka's@pbjpkas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ***********************************************************************/
#include <stdint.h>
#include <SPI.h>

// LTC1660CN 8ch 10-bit DAC
#define DAC_CHANNEL            8
#define DAC_DATA_MIN           0
#define DAC_DATA_MAX        1023
#define DAC_ADDRESS_MIN        0
#define DAC_ADDRESS_MAX       15

// Num of DAC
#define NUM_OF_DAC             2 // 2個
// DAC CSLD PIN
#define DAC_0_CSLD_PIN         2
#define DAC_1_CSLD_PIN         3

// Command Line Interface
#define ERR_OK                 0
#define ERR_INVALID           -1 // 不正
#define ERR_NULL              -2 // 引数がNULL
#define ERR_MALLOC            -3 // mallocの戻り値がNULL

void setup();
void loop();

/***********************************************************************
  LTC1660CN
 ***********************************************************************/
class ltc1660cn
{
    public:
        ltc1660cn(){};
        ltc1660cn(uint8_t dac_id, uint8_t csld_pin);
        ~ltc1660cn();
        init_dac();
        get_dac();
        set_dac(uint8_t ch, uint16_t value);
    private:
        uint8_t  dac_id;
        uint8_t  csld_pin;
        uint16_t value_array[DAC_CHANNEL] = {0};
        write_dac(uint8_t ch, uint16_t value);
};

ltc1660cn::ltc1660cn(uint8_t dac_id, uint8_t csld_pin)
{
    this->dac_id   = dac_id;
    this->csld_pin = csld_pin;
}

ltc1660cn::~ltc1660cn()
{
    //
}

ltc1660cn::init_dac()
{
    set_dac(15, 0); //Load ALL DACs with Same Code
}

ltc1660cn::get_dac()
{
    for(int i=0; i<DAC_CHANNEL; i++)
    {
        Serial1.print(value_array[i]);
        Serial1.print(" ");
    }
    Serial1.print("\n");
}

ltc1660cn::set_dac(uint8_t ch, uint16_t value)
{
    if( ch > DAC_ADDRESS_MAX )
    {
        Serial1.print("### invalid ch: ");
        Serial1.print(ch);
        Serial1.print(" ");
        return ERR_INVALID;
    }

    if( value > DAC_DATA_MAX )
    {
        Serial1.print("### invalid value: ");
        Serial1.print(value);
        Serial1.print(" ");
        return ERR_INVALID;
    }

    if(ch == 15) //Load ALL DACs with Same Code
    {
        for(int i=0; i<DAC_CHANNEL; i++)
        {
            value_array[i] = value;
        }
    }
    else if(ch >= 1 && ch <= 8)
    {
          value_array[ch-1] = value;
    }
    write_dac(ch, value);
    return ERR_OK;
}

ltc1660cn::write_dac(uint8_t ch, uint16_t value)
{
    uint8_t data[2] = {0};

    data[0] = ch << 4 | uint8_t(value >> 6);
    data[1]  = uint8_t(value << 2);

    digitalWrite(csld_pin, LOW);
    for(int i=0; i<2; i++)
    {
        SPI.transfer(data[i]);
        //Serial1.println(data[i], HEX);
    }
    digitalWrite(csld_pin, HIGH);
}

ltc1660cn LTC1660CN[NUM_OF_DAC];

void dac_init()
{
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));//1MHz

    pinMode(DAC_0_CSLD_PIN, OUTPUT);
    pinMode(DAC_1_CSLD_PIN, OUTPUT);
    digitalWrite(DAC_0_CSLD_PIN, HIGH);
    digitalWrite(DAC_1_CSLD_PIN, HIGH);
    LTC1660CN[0] = ltc1660cn(0,DAC_0_CSLD_PIN);
    LTC1660CN[1] = ltc1660cn(1,DAC_1_CSLD_PIN);
    LTC1660CN[0].init_dac();
    LTC1660CN[1].init_dac();
}

/***********************************************************************
  Function Prototype : Command Mode
 ***********************************************************************/
#define CMD_OK          ERR_OK
#define CMD_BUF_LENGTH      64 // 63+1
#define CMD_MAX_LENGTH      64 // 63+1
#define ARG_MAX_LENGTH      64 // 63+1

void cmd_print_help(void);
void cmd_print_ver(void);
int  cmd_execute(bool *echoback, char *buf);
void cmd_rx_data(void);

/***********************************************************************
  Function : Command Mode
 ***********************************************************************/
void cmd_print_help(void)
{
    Serial1.println(F("Available Command:"));
    Serial1.println(F("help, ?                  : print Help Messages"));
    Serial1.println(F("ver                      : print Version Information"));
    Serial1.println(F("echo <flag>              : flag(0 to off, 1 to on)"));
    Serial1.println(F("dacset <id> <ch> <value> : id(0-1), ch(1-8,14,15), value(0-1023)"));
    Serial1.println(F("dacget <id>              : print DAC value"));
}
void cmd_print_ver(void)
{
    Serial1.print("This is ");
    Serial1.print(__FILE__);
    Serial1.print(" ");
    Serial1.print("Build at ");
    Serial1.print(__DATE__);
    Serial1.print(" ");
    Serial1.print(__TIME__);
    Serial1.print("\n");
}

int cmd_execute(bool *echoback, char *buf)
{
    int i, x, y;
    unsigned int ux;
    int return_val = CMD_OK;
    int echo_on = false;
    char cmd[CMD_MAX_LENGTH];
    char arg1[ARG_MAX_LENGTH];
    char arg2[ARG_MAX_LENGTH];
    char arg3[ARG_MAX_LENGTH];

    strcpy(cmd, "");
    strcpy(arg1, "");
    strcpy(arg2, "");
    strcpy(arg3, "");
    sscanf(buf, "%s %s %s %s", &cmd, &arg1, &arg2, &arg3);

    if     (strcmp(cmd, "help")==0){ cmd_print_help(); }
    else if(strcmp(cmd, "?"   )==0){ cmd_print_help(); }
    else if(strcmp(cmd, "ver" )==0){ cmd_print_ver();  }

    else if(strcmp(cmd, "echo")==0)
    {
        if(atoi(arg1))
        {
            *echoback = true;
        }
        else
        {
            *echoback = false;
        }
    }
    else if(strcmp(cmd, "dacset")==0)
    {
        if( abs(atoi(arg1)) >= NUM_OF_DAC )
        {
            Serial1.print("### invalid DAC ID. ###");
            return_val = ERR_INVALID;
        }
        else
        {
            return_val = LTC1660CN[atoi(arg1)].set_dac(atoi(arg2), atoi(arg3));
        }
    }
    else if(strcmp(cmd, "dacget")==0)
    {
        if( abs(atoi(arg1)) >= NUM_OF_DAC )
        {
            Serial1.print("### invalid DAC ID. ###\n");
            return_val = ERR_INVALID;
        }
        else
        {
            LTC1660CN[atoi(arg1)].get_dac();
        }
    }
    else
    {
        return_val = ERR_INVALID;
    }

    return return_val;
}

void cmd_rx_data(void)
{
    static int i = 0;
    static char buf[CMD_BUF_LENGTH];
    static bool echoback = true;
    int return_val = CMD_OK;

    if(Serial1.available())
    {
        buf[i] = Serial1.read();
        if(echoback) Serial1.print(buf[i]); //echo-back

        if ( (buf[i] == 0x08) or (buf[i] == 0x7f) ) //BackSpace, Delete
        {
            buf[i] = '\0';
            if(i) i--;
        }
        else if( (buf[i] == '\r') or (buf[i] == '\n') )
        {
            if(echoback) Serial1.print( F("\n") );
            buf[i] = '\0';
            return_val = cmd_execute(&echoback, &buf[0]);
            for(i=0; i<CMD_BUF_LENGTH; i++) buf[i] = '\0';
            i=0;

            if(return_val != ERR_OK)
            {
                Serial1.print(F("?\n"));
            }
            else
            {
                Serial1.print(F("OK\n"));
            }
        }
        else
        {
            i++;
            if(i>=CMD_BUF_LENGTH)
            {
                Serial1.print(F("### CMD BUFFER FULL, CLEAR. ###\n"));
                for(i=0; i<CMD_BUF_LENGTH; i++) buf[i] = '\0';
                i=0;
            }
        }
    }
}

/***********************************************************************
  Function : setup and loop
 ***********************************************************************/
void setup()
{
    Serial1.begin(115200);
    SPI.begin();
    dac_init();
}

// the loop routine runs over and over again forever
void loop()
{
    cmd_rx_data();
}

7. 動作確認

負荷として1kΩの抵抗をDAC Eに接続し動作確認を行いました。1kΩはDACの出力が5Vのときに負荷電流が定格上限の5mAとなる値です。

動作確認

オシロスコープはRIGOLのDS1104Zを使用しました。プローブの配線は以下の通りです。

オシロスコープのチャンネル 接続
CH1 Chip Select
CH2 SCK
CH3 MOSI
CH4 DAC Eの出力

7.1 0V→5V

コマンド"dacset 1 5 1023"を実行したときのオシロスコープのスクリーンショットを以下に示します。

0V→5V

以下の4点を確認できました。

  • DACのADDRESS:0b0101
  • DACのINPUT CODE:0b1111111111
  • DACのDON'T CARE:0b00
  • Chip SelectをEnableにしてからDAC Eの出力が設定値になるまでの時間:29.6μs

7.2 5V→0V

コマンド"dacset 1 5 0"を実行したときのオシロスコープのスクリーンショットを以下に示します。

5V→0V

以下の4点を確認できました。

  • DACのADDRESS:0b0101
  • DACのINPUT CODE:0b0000000000
  • DACのDON'T CARE:0b00
  • Chip SelectをEnableにしてからDAC Eの出力が設定値になるまでの時間:45.0μs

7.3 valueに341を設定

10進数の341は2進数で0101010101となります。MOSIのデータが意図したとおりに出力されることを確認します。

コマンド"dacset 1 5 341"を実行したときのオシロスコープのスクリーンショットを以下に示します。

DAC Value 341

以下4点を確認できました。

  • DACのADDRESS:0b0101
  • DACのINPUT CODE:0b0101010101
  • DACのDON'T CARE:0b00
  • Chip SelectをEnableにしてからDAC Eの出力が設定値になるまでの時間:25.7μs

7.4 DACの線形性

無負荷および負荷抵抗1kΩのときのDACの設定値と出力電圧を調べました(付録Aもご参照ください)。無負荷時はリファレンス電圧まで振れましたが1kΩ負荷時はリファレンス電圧の約93%で頭打ちとなりました。

DAC linearity(no-load)
DAC linearity(1kΩ)

8. おわりに

Arduino LeonardoにDAC(LTC1660CN)を接続しコマンドで制御してLチカできました。

付録A. DACの線形性測定のテストスクリプト

以下の手順1~3をDACの設定を変えながら繰り返し、DACの出力電圧をオシロスコープで読み取ります。

  1. オシロスコープをSINGLEでトリガ待ちする(Chip Selectでトリガをかける)
  2. DACを設定する
  3. オシロスコープのカーソルAのY Valueを取得する(Chip Selectイネーブル後100μsの、DACの出力が安定している箇所の値を取得する)

測定例

コマンド
> python test-runner.py dac-sokutei
dac-sokutei.csv
open_dso
dso,*IDN?
open_uart,com7,0
#
dso,:single
sleep,2
send,dacset 1 5 0
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 63
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 127
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 191
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 255
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 319
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 383
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 447
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 511
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 575
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 639
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 703
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 767
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 831
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 895
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 959
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 1023
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2
#
dso,:single
sleep,2
send,dacset 1 5 0
sleep,2
dso,:CURSor:TRACk:AYValue?
sleep,2

付録B. Lチカで始めるテスト自動化・記事一覧

  1. Lチカで始めるテスト自動化
  2. Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上
  3. Lチカで始めるテスト自動化(3)オシロスコープの組込み
  4. Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)
  5. Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み
  6. Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定
  7. Lチカで始めるテスト自動化(7)タイムスタンプの保存
  8. Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み
  9. Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み
  10. Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上
  11. Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作
  12. Lチカで始めるテスト自動化(12)書籍化の作業メモ
  13. Lチカで始めるテスト自動化(13)外部プログラムの呼出し
  14. Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する
  15. Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する
  16. Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する
  17. Lチカで始めるテスト自動化(17)コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作する
  18. Lチカで始めるテスト自動化(18)秋月のIoT学習HATキットの圧電ブザーでテスト終了時にpass/failに応じてメロディを流す
  19. Lチカで始めるテスト自動化(19)Webカメラの映像を録画しながらテストスクリプトを実行する
  20. Lチカで始めるテスト自動化(20)複数のカメラ映像の同時録画
  21. Lチカで始めるテスト自動化(21)キーボード入力待ちの実装

電子書籍化したものを技術書典で頒布しています。
1. Lチカで始めるテスト自動化
2. Lチカで始めるテスト自動化 -2- リレー駆動回路の設計 (※書き下ろし)
3. Lチカで始めるテスト自動化 -3- Raspberry Pi Zero WHとIoT学習HATキットで作るテストランナー


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

  2. Lチカで始めるテスト自動化(3)オシロスコープの組込み 

  3. LTC1660のデータシートによると "Each of the eight rail-to-rail output amplifiers contained in these parts can source or sink up to 5mA." とのことです。 

  4. LEDの順方向電圧Vfは色や型番によって約1.8[V]~約3.7[V]まで様々です。詳しくはデータシートでご確認ください。 

0
0
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
0
0