##1. はじめに
スタティック点灯1やダイナミック点灯2に続き、今回はDAC(D/A Converter)をコマンドで制御してLチカを試します。
##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を使用しました。データシートよりブロックダイアグラムを引用します。
###3.1 制御方法
LTC1660CNはSPI(Serial Peripheral Interface)で制御します。データは16bit、MSB Firstで、クロックの立上りエッジでデータが読み込まれます。
###3.2 アドレス/コントロール
LTC1660CNは4bitのアドレス空間があります。0b0001~0b1000がDAC A~DAC Hに対応するほか、0b1110でスリープへ、0b1111ですべてのDACチャンネルに同じ設定を行います。
##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
##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のソースを以下に示します。
/***********************************************************************
* 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"を実行したときのオシロスコープのスクリーンショットを以下に示します。
以下の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"を実行したときのオシロスコープのスクリーンショットを以下に示します。
以下の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"を実行したときのオシロスコープのスクリーンショットを以下に示します。
以下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%で頭打ちとなりました。
##8. おわりに
Arduino LeonardoにDAC(LTC1660CN)を接続しコマンドで制御してLチカできました。
##付録A. DACの線形性測定のテストスクリプト
以下の手順1~3をDACの設定を変えながら繰り返し、DACの出力電圧をオシロスコープで読み取ります。
- オシロスコープをSINGLEでトリガ待ちする(Chip Selectでトリガをかける)
- DACを設定する
- オシロスコープのカーソルAのY Valueを取得する(Chip Selectイネーブル後100μsの、DACの出力が安定している箇所の値を取得する)
> python test-runner.py dac-sokutei
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チカで始めるテスト自動化・記事一覧
- Lチカで始めるテスト自動化
- Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上
- Lチカで始めるテスト自動化(3)オシロスコープの組込み
- Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)
- Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み
- Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定
- Lチカで始めるテスト自動化(7)タイムスタンプの保存
- Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み
- Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み
- Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上
- Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作
- Lチカで始めるテスト自動化(12)書籍化の作業メモ
- Lチカで始めるテスト自動化(13)外部プログラムの呼出し
- Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する
- Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する
- Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する
- Lチカで始めるテスト自動化(17)コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作する
- Lチカで始めるテスト自動化(18)秋月のIoT学習HATキットの圧電ブザーでテスト終了時にpass/failに応じてメロディを流す
- Lチカで始めるテスト自動化(19)Webカメラの映像を録画しながらテストスクリプトを実行する
- Lチカで始めるテスト自動化(20)複数のカメラ映像の同時録画
- Lチカで始めるテスト自動化(21)キーボード入力待ちの実装
電子書籍化したものを技術書典で頒布しています。
- Lチカで始めるテスト自動化
- Lチカで始めるテスト自動化 -2- リレー駆動回路の設計 (※書き下ろし)
- Lチカで始めるテスト自動化 -3- Raspberry Pi Zero WHとIoT学習HATキットで作るテストランナー