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

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

![DAC回路](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/a94b7872-8ed6-fb5a-79d9-1a5b371e6245.jpeg)

##2. LEDの点灯をDACで制御する原理
DACの出力に抵抗Rを介してLEDを接続します。

![回路図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/995bfb6e-b95e-3ad6-331e-f742fdda53ee.png)

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

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

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

```math
\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](https://www.arduino.cc/en/Main/ArduinoBoardDue)というDACを内蔵したモデルがあるのですが、手持ちのArduino LeonardoはDACを内蔵していないため[秋月電子通商](https://akizukidenshi.com)で販売されている8ch 10bitの[LTC1660CN](https://akizukidenshi.com/catalog/g/gI-02794/)を使用しました。[データシート](https://akizukidenshi.com/download/ltc1660cn.pdf)よりブロックダイアグラムを引用します。

![LTC1660CN Block Diagram](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/97762b1f-96e0-235b-7591-7748fa51319e.png)

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

![LTC1660CN Register Loading Sequence](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/4ff9f809-4c7d-5fc8-b25d-c7ebf08af95f.png)

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

![LTC1660CN Address/Control Functions](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/6c697779-d4ba-05f2-8187-a8c7409c5634.png)

##4. Arduiono Leonardoとの接続
Arduino LeonardoとLTC1660CNの接続を以下に示します。ArduinoのSPIの詳細は[SPI library](https://www.arduino.cc/en/Reference/SPI)をご参照ください。

|Arduino Leonardo|LTC1660CN
|----------------+---------
|5V              |V<sub>CC</sub>, REF, CLR
|GND             |GND
|SCK(ICSP-3)     |SCK
|MOSI(ICSP-4)    |D<sub>IN</sub>
|Digital #2      |1つめのLTC1660CNのCS/LD
|Digital #3      |2つめのLTC1660CNのCS/LD

ICSP端子は以下の写真をご参考ください。
![icsp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/bc3b229a-d2b4-ec1b-15af-ef1b1bbe5490.jpeg)

##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以下の整数で指定します。

```txt:例
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のソースを以下に示します。

```c: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となる値です。

![動作確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/a1121660-2e28-79d9-82e0-a62c976df8f1.jpeg)

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

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

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

![0V→5V](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/4f665325-ec13-37b3-5629-965a88ffca25.png)

以下の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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/d29c0335-da61-9b28-7e59-4c4c24d6c9c4.png)

以下の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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/1fed5271-d511-5b9d-78c3-0149b9c9e53d.png)

以下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)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/4fa1371a-655d-5cc6-8ff9-f16f69f8edb7.png)
![DAC linearity(1kΩ)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/2780e30f-73eb-bdad-2ad6-65eba0ceb1f1.png)

##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の出力が安定している箇所の値を取得する)

![測定例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/76edede2-e0df-8381-b99b-81847309d7e7.png)

```sh:コマンド
> python test-runner.py dac-sokutei
```

```sh: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チカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
2. [Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上](https://qiita.com/pbjpkas/items/337bce392281ee5f0526)
3. [Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)
4. [Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)](https://qiita.com/pbjpkas/items/a02c4128baf5a3f2a46c)
5. [Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み](https://qiita.com/pbjpkas/items/9af9e6441e1e20c5909e)
6. [Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定](https://qiita.com/pbjpkas/items/405d55f809da1221b867)
7. [Lチカで始めるテスト自動化(7)タイムスタンプの保存](https://qiita.com/pbjpkas/items/8e3c4330f4a81d30bd53)
8. [Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み](https://qiita.com/pbjpkas/items/58f84c347e44fb9d5635)
9. [Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み](https://qiita.com/pbjpkas/items/7a4cb4c59886b1fd2cf9)
10. [Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上](https://qiita.com/pbjpkas/items/c011d7ad6f98bc1458d5)
11. [Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作](https://qiita.com/pbjpkas/items/fa6d2956ea3dd18b3584)
12. [Lチカで始めるテスト自動化(12)書籍化の作業メモ](https://qiita.com/pbjpkas/items/e76a763de7f496d5430c)
13. [Lチカで始めるテスト自動化(13)外部プログラムの呼出し](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d)
14. [Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する](https://qiita.com/pbjpkas/items/4f023917f31f6096eb6b)
15. [Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する](https://qiita.com/pbjpkas/items/a2e7551ae10f7dce2505)
16. [Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する](https://qiita.com/pbjpkas/items/0a48ca71a2de0042e188)
17. [Lチカで始めるテスト自動化(17)コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作する](https://qiita.com/pbjpkas/items/3df58743644128a11b9e)
18. [Lチカで始めるテスト自動化(18)秋月のIoT学習HATキットの圧電ブザーでテスト終了時にpass/failに応じてメロディを流す](https://qiita.com/pbjpkas/items/43a609581cc68b7ea2fd)
19. [Lチカで始めるテスト自動化(19)Webカメラの映像を録画しながらテストスクリプトを実行する](https://qiita.com/pbjpkas/items/589243af5c9ff0dc933c)
20. [Lチカで始めるテスト自動化(20)複数のカメラ映像の同時録画](https://qiita.com/pbjpkas/items/cdfc0238b54bf197e98a)
21. [Lチカで始めるテスト自動化(21)キーボード入力待ちの実装](https://qiita.com/pbjpkas/items/9e3c21c3cd375635568a)

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

[^1]: [Lチカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
[^2]: [Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)
[^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]まで様々です。詳しくはデータシートでご確認ください。