« タミヤ★★ミニ四駆用充電池「NEO CHAMP」950mAh 800サイクル目 | トップページ | 女房実家から持って帰ってきた「ミニ灯籠」と、雨のツツジ »

2022年4月22日 (金)

Arduino UNOのA/D入力:PWMでD/A出力してA/D入力を試す

A/DとD/A(PWMを使って)、同じ基準電圧で出力・入力すると
スケールが同じになります。
ラズパイ・ピコで試したのが
  ・Arduino IDEでRaspberry Pi Pico:PWMでD/A出力してA/D入力を試す
結果・・・
 「こりゃあかんで」
 「まともに使われへんやん」
ということに。
ピコでまともなA/Dしたい時は外付けICを使うのが定石
になるかと思います。

で、同じ考え方をArduino UNOで試してみました。
UNOに乗っているマイコンチップはATmega328P。
過去、いろんなツールに使ってきましたが、ピコの
ようないびつなA/D変換結果には出くわしていません。
外部から与えた電圧(などの信号)で2点校正すれば、
安定した読みが得られるぞ、ということで、回路と
ソフトを作ってきました。

あらためて試してみました。
まずは外部から基準電圧を与えるため、こんな回路で
試します。
A11_20220422130301
使った基準電圧ICはMCP1525とMCP1541。
それぞれの実値は、2.4976Vと4.0777Vでした。

10bitのPWMを0~1023、step=1で可変。
A/Dの平均回数は16回。
その結果。
Cc1_20220422130401

3LSBほどの差が生じましたが、「段」や「飛び」
はありません。
  ※0V付近に不感帯が発生。
   オペアンプによるものは1.0~1.5mVです。

次がチップに内蔵された1.1V基準電圧源。
AREF端子から外に出し、オペアンプU1Bで受けて
D/Aの基準電圧にします。
こんな回路。
A12_20220422130501

AIN0入力に入れるCRフィルタの影響も見てみました。
結果のグラフ。
Cc2_20220422130601
ノイズ除去用として良く用いる「10KΩ + 0.1uF」の
フィルタ。
大きな変換値への影響は無さそうです。

他のArduino UNO基板ではどうかと試しました。
グラフはスムージング処理を加えて描きました。
Cc3_20220422130701

2点校正:線形補間すれば、10bitの分解能を十分に
生かせる特性です。

※関連
線形補間って「LERP」って言うんだ!
Arduino 10bit A/D値をmap関数でスケーリングする例
ミスが広まる 1/1023 vs 1/1024
Arduino なんとかして誤用を正したい:A/Dの1/1023とmap関数
Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMを使おう

/*****  PWM出力とA/D入力のテスト *****/
// Arduino UNOで
// A0でA/D入力
// PWM周波数は1.95kHz, 10bit分解能 D9(PB1) OC1A出力
// Vrefに2.5V基準電圧ICを接続 → 内部1.1Vで ★1
/***** 送信データ *****/
char tx_str[64]; // 送信文字列
/***** シリアル1行入力 *****/
#define RXBF_SIZ 32 // 文字バッファ文字数
char rx_bff[RXBF_SIZ+1]; // 受信文字バッファ (+null)
byte f_rxok; // 受信データありフラグ
/***** シリアル1行受信 *****/
// CRでターミネート f_rxokを1に
// エコーバックなしに
// なし→BSで1文字戻す
void rxbff(void)
{
static byte cnt = 0; // 受信文字数
char c;
if(Serial.available()){ // 受信データあり
if(f_rxok == 0){ // 前データ受信処理した
c = Serial.read(); // 1文字読み出し
if(c == '\r'){ // CR?
rx_bff[cnt] = '\0'; // nullを最後に
// Serial.println(); // 改行
f_rxok = 1; // 受信成功
cnt = 0; // 最初から
}
// else if(c == '\x08'){ // BS? (処理なしに)
// if(cnt > 0){
// cnt--; // 1文字戻す
// Serial.print("\b \b"); // BS,space,BS
// }
// }
else{ // 文字
if((cnt < RXBF_SIZ) && // バッファサイズ内
(isprint(c))){ // 表示可能文字0x20~0x7E
// Serial.write(c); // エコーバック
rx_bff[cnt] = c; // バッファに入れる
cnt++; // 1文字進める
}
}
}
}
}
/***** Break処理付のdelay *****/
// シリアル受信あればdelayを中断
void bkdelay(word dly)
{
word tn, t;
tn = (word)millis(); // now
while(1){
t = (word)millis() - tn; // 経過時間
if(t >= dly) break;
if(Serial.available()) break; // 受信データ
if(f_rxok) break; // CR入力
}
}
/***** 平均処理付A/D入力 *****/
// avr:平均回数
// Raspberry Pi Picoは12bit
// Arduino UNOは10bit
// D2出力は処理タイミングチェック用
// ノイズ減のためコメントアウト
word adavr(word avr)
{
word ad;
word i;
uint32_t add; // 平均加算値
if(avr == 0) avr = 1; // 0なら1回に
add = 0;
for(i = 0; i < avr; i++){
// digitalWrite(2, HIGH); // D2 H
ad = analogRead(A0); // 生A/D値
// digitalWrite(2, LOW); // D2 L
add += (uint32_t)ad; // 加算
}
ad = (word)(add / avr); // 平均値
return ad; // 10bit A/D値
}

/***** D9:PB1:OC1Aへのアナログ出力 *****/
// 10bit値 0~1023
#define PWMTOP 1023 // 10bit max値
void analogWrite9(word d)
{
if(d > PWMTOP) d = PWMTOP;
OCR1A = PWMTOP - d; // PB1設定
}

/***** SETUP *****/
void setup() {
Serial.begin(9600); // 通信
pinMode(2, OUTPUT); // D2
pinMode(9, OUTPUT); // D9 PC1A PWM出力
pinMode(LED_BUILTIN, OUTPUT); // LED
// PWMでD/A出力
// タイマー1 PWM出力 (A:PB1,B:PB2)
TCCR1A = 0b11000010;
// |||| ++---- PWM ICR1 モード
// ||++-------- Port動作
// ++---------- PWM A (Negモード)
TCCR1B = 0b00011010;
// || ||+++---- クロックセレクト 16MHz/8=2MHz
// || ++------- PWM ICR1 モード
// |+---------- ICES1
// +----------- ICNC1
ICR1 = PWMTOP; // 10bit
OCR1A = 0xFFFF; // PWM off 0Vに
// A0入力 Vrefは外部 → 内部に
// analogReference(EXTERNAL); // 外部 ★1
analogReference(INTERNAL); // 内部1.1V
analogRead(A0); // 1回変換しておく
}

// 繰り返しデータ
word ad; // A/D値
word da; // D/A:PWM設定値
char *s; // 入力文字列 最初の
word n; // 文字列番号 5つまで
char *s_in[5]; // 文字列5つ
word d_in[5]; // 入力 start,end,step,avr,wait
byte j_exc; // 実行区分

/***** LOOP *****/
void loop() {
byte f_xLED = 0; // LED点滅用
Serial.println("# A/D test out:PWM D9 in:AD0");
delay(500);
while(1){
f_xLED ^= 1; // LED トグル
digitalWrite(LED_BUILTIN, f_xLED); // LED出力
rxbff(); // 1行受信処理
switch(j_exc){ // 実行区分
case 0: // コマンドプロンプト表示
Serial.print("#Start End Step Avr Wait >");
d_in[2] = 16; // step値を16に
d_in[3] = 1; // 平均回数=1で
d_in[4] = 200; // wait時間を200msに
j_exc = 1;
break;
case 1: // コマンド入力
if(f_rxok){
f_rxok = 0;
Serial.println(); // 改行
n = 0;
s = strtok(rx_bff, " ,"); // 最初の文字分離
while(s != NULL){
s_in[n] = s;
d_in[n] = atoi(s_in[n]); // 数字に
s = strtok(NULL, " ,"); // 次文字分離
n++;
if(n >= 5) break; // 5つまで
}
if(n == 0){ // 数値入力なし
j_exc = 0; // プロンプトから
}
else if(n == 1){ // start一つだけ
da = d_in[0]; // D/A値
analogWrite9(da); // PWM出力して
delay(1000); // wait
j_exc = 2; // A/D入力繰り返しへ
}
else{ // end,step,waitで
j_exc = 3; // PWM繰り返しへ
}
}
break;
case 2: // A/D入力表示続行
ad = analogRead(A0); // A/D値
sprintf(tx_str, "D/A:%4d A/D:%4d",
da, ad);
Serial.println(tx_str);
bkdelay(1000); // 1秒
if(f_rxok){ // break?
f_rxok = 0;
Serial.println("#Break");
j_exc = 0;
}
break;
case 3: // start,end,step,avr,wait
da = d_in[0]; // start値
analogWrite9(da); // PWM出力
Serial.println("#D/A A/D diff");
bkdelay(1000); // 1秒
j_exc = 4; // A/D入力繰り返しへ
break;
case 4: // loop
analogWrite9(da); // PWM出力
bkdelay(d_in[4]); // wait ミリ秒
ad = adavr(d_in[3]); // avr A/D平均処理
sprintf(tx_str, "%4d %4d %5d",
da, ad, (ad - da)); // 差も出力
Serial.println(tx_str);
if(da >= d_in[1]){ // endと比較 おわり?
Serial.println(); // 改行x2
Serial.println();
j_exc = 3; // A/D入力繰り返しへ
}
da += d_in[2]; // +step
if(f_rxok){ // break?
f_rxok = 0;
Serial.println("#Break");
j_exc = 0;
}
break;
}
}
}

PWM出力、8bitしか扱えないanalogWrite()は使っていません。
タイマー1を使ったPWM出力、その初期化手順はsetup()を、
出力方法はanalogWrite9()を見てください。

12_20220422154401
ブレッドボードに2つあるIC、上のほうがオペアンプ。
下側がアナログマルチプレクサ。

|

« タミヤ★★ミニ四駆用充電池「NEO CHAMP」950mAh 800サイクル目 | トップページ | 女房実家から持って帰ってきた「ミニ灯籠」と、雨のツツジ »

Arduino」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« タミヤ★★ミニ四駆用充電池「NEO CHAMP」950mAh 800サイクル目 | トップページ | 女房実家から持って帰ってきた「ミニ灯籠」と、雨のツツジ »