ArduinoのanalogWrite 1/255なの?
ミスが広まる 1/1023 vs 1/1024 はA/Dコンバータのスケーリングの話。
Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ
はタイマーコンペアレジスタ設定の話。
そして今度は「analogWrite」、ArduinoのPWM出力の話をちょっと。
Arduino-UNOだと3つのタイマーを使った6つのPWM出力が可能です。
仕様では分解能8bit。
void analogWrite(uint8_t pin, int val)
という関数で使います。
ピン番号で使うタイマーが決まり、例えば5,6ピンならタイマー0が
用いられます。
タイマーのクロックが250kHzで8ビットですので、1.024ms周期で
1ビットが4usの分解能になります。
valの値がPWMのデューティー比で、設定できるのは0~255。
0で出力LOW固定になり、255で出力HIGH固定。
128でデューティー50%の方形波。
analogWriteを単にPWM出力ということではなく「D/Aコンバータ」
として使った時、その出力範囲をどう考えればよいでしょうか?
(「1/1023 vs 1/1024」に近い話になります)
8ビットの0~255のデータを、可変範囲が0~5VのD/Aコンバータで
出力する時、最大値は「(255×5V)/256」で、フルスケールの5Vより
約20mV低いおよそ4.98Vがmax値となります。
ところがanalogWriteは「0V=LOWに張り付いてパルス無し」
「5V=HIGHに張り付いてパルス無し」まで出力できます。
本来ならフルスケール「(255×1.024ms)/256」で、
1020usのHIGHパルスに4usのLOWパルスが残るはず。
それがなぜか255でHIGHに張り付いちゃいます。
1/256ではなく1/255で処理されているのじゃないかと考え
られるのです。
analogWriteは「wiring_analog.c」内で処理されていますので、
ソースをちょいと覗いてみると・・・
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void analogWrite(uint8_t pin, int val)
{
pinMode(pin, OUTPUT); // 指定ピンを出力に
if (val == 0){ // ★1
digitalWrite(pin, LOW); // 値が0ならLOWに張り付き
}
else if (val == 255){ // ★2
digitalWrite(pin, HIGH); // 値が255ならHIGHに張り付き
}
else { // ArduinoCPUによる区別
switch(digitalPinToTimer(pin)) { // その代表で
case TIMER0A: // タイマー0
sbi(TCCR0A, COM0A1); // OC0Aピン非反転PWM出力モード
OCR0A = val; // set pwm duty ★3 値が1~254の時
break;
:
}
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
★1と★2で0と255を分離処理して、★3で1~254の時だけ
PWMパルスが出るようにしています。
そこで、タイマー0のanalogWriteに対して、0~255の数値を順に
与えてみると・・・周期1.024msのパルスに対して、
0ならずっとLOW
1だと8usのHIGHパルス ▼1
2だと12usのHIGHパルス
:
253だと8usのLOWパルス
254だと4usのLOWパルス
255だとずっとHIGH
が観察されます。
タイマー0に関して、値0と1のところ▼1で、ほんとなら4usステップ
になって欲しいのが8usとなっていて、値1~255とは異なるピッチに
なっていることがわかります。
※この部分の注意点
これ、タイマー0だからなんです。
OCR0A = val
とコンペアレジスタに設定値を与えているから。
OCR0A = val - 1
にすると、値1で4usのHIGHパルスが得られます。
そして★2の処理を無くせば255で4usのLOWパルスとなり、
「(255×1.024ms)/256」、「(255×5V)/256」の値が得られます。
なお、タイマー0が「8bit高速PWMモード」で初期化されている
ことに注意してください。
タイマー1とタイマー2は「8bit位相基準PWMモード」に設定されて
いて、動作が異なるのです。
このあたりの差が、数値に対するPWM波形出力に影響を与えます。
analogWriteにD/A変換機として純粋な精度を求める場合はちょいと
ご注意を。
ハードウェアマニュアルをよく読み、テストプログラムを書いて
動きを確かめてということで。
※追記
デューティー50%の方形波を得ようとして
analogWrite(6,128);
しても、HIGH=512us、LOW=512usの波形にはなりません。
HIGHが516us、LOWが508usになり、デューティ50.4%くらいの
方形波が出てきます。
きっちり50%にしたいのなら、
analogWrite(6,127);
です。 (タイマー0で)
※PWMでのD/A変換、こんな回路を使ってます。
Arduinoの5Vは不安定なので、基準電圧ICで4.096Vとか
2.5Vなどを発生。
PWM波をアナログマルチプレクサ(74HC4053など)を使って
GND-VREF電圧を切り替える。
それをLPFで平滑。
OP-AMPでバッファして電圧出力に。
タイマー1を使うと分解能を10bitとか12bitに拡張できる。
analogWriteじゃなく自前のプログラムで制御。
※PWM出力の様子をオシロスコープで観察する
ためのテストプログラム。 (注:スペースを全角で)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#include "wiring_private.h"
// I/O MACRO
#define PB0_H (PORTB |= (1 << PB0)) // (!!!)PB0 H/L D8
#define PB0_L (PORTB &= ~(1 << PB0))
#define PB4_H (PORTB |= (1 << PB4)) // (!!!)PB4 H/L D12
#define PB4_L (PORTB &= ~(1 << PB4))
#define PB5_H (PORTB |= (1 << PB5)) // (!!!)PB5 H/L D13
#define PB5_L (PORTB &= ~(1 << PB5))
/***** タイマー0 オーバーフロー回数読み出し *****/
extern volatile unsigned long timer0_overflow_count;
unsigned long readovf0(void)
{
unsigned long t;
cli(); // 割込禁止
t = timer0_overflow_count;
sei(); // 割込許可
return t;
}
/***** PWM出力 *****/
// val : PWM設定値 0~255
// OC0A,OC1A,OC2Aに出力
void pwm3ch(int val)
{
analogWrite(6,val); // タイマー0 OC0A
analogWrite(9,val); // タイマー1 OC1A
analogWrite(11,val); // タイマー2 OC2A
}
/***** セットアップ *****/
// ATmega328Pのレジスタを直接制御
void setup()
{
cli(); // 割込禁止
// I/Oイニシャル
PORTB = 0b00000000; // data/pull up
DDRB = 0b00111111; // port指定
// |||||+---- PB0 IO8 out test pulse
// ||||+----- PB1 IO9 out OC1A
// |||+------ PB2 IO10 out OC1B
// ||+------- PB3 IO11 out OC2A
// |+-------- PB4 IO12 out test pulse
// +--------- PB5 IO13 out (LED) test pulse
PORTD = 0b00000000; // data/pull up
DDRD = 0b11111100; // port指定
// |||||||+---- PD0 IO0 RXD
// ||||||+----- PD1 IO1 TXD
// |||||+------ PD2 IO2 out
// ||||+------- PD3 IO3 out OC2B
// |||+-------- PD4 IO4 out
// ||+--------- PD5 IO5 out OC0B
// |+---------- PD6 IO6 out OC0A
// +----------- PD7 IO7 out
sei(); // 割込許可
}
/***** LOOP *****/
void loop()
{
unsigned long ovf0;
unsigned long ovfck;
byte cnt = 0; // オーバーフローカウンタ
byte kubun = 0; // PWM出力区分
static int val[]={ // PWM設定値
0, 1, 2, 127, 128, 253, 254, 255, // 8つのPWM値
}; // 0:LOW~255:HIGH
ovf0 = readovf0(); // オーバーフロー回数
while(1){
ovfck = readovf0();
if(ovf0 != ovfck){ // 変化あり
ovf0 = ovfck; // 1.024msタイマー0オーバーフロー
PB0_H; // 1.024msごとにパルス出力
if(cnt == 0){
PB4_H; // 先頭カウントの時
if(kubun == 0) PB5_H; // 区分0の時パルス
else PB5_L;
pwm3ch(val[kubun]); // 区分によりPWM出力
}
else{
PB4_L;
}
cnt++;
if(cnt >= 6){ // 6回オーバーフロー
cnt = 0;
kubun++; // PWM区分
if(kubun > 7) kubun = 0; // 0~7
}
PB0_L;
}
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
・timer0_overflow_countがタイマー0割り込みでインクリメントされるので、
これを見て1.024ms経過を検出。
・PB0は1.024msごとにパルス出力。
PB4はPWM値設定ごとに、PB5は区分一周でパルス出力。
オシロのトリガー源に。
・タイマー0のPWMはこのパルス周期に同期しているが、
タイマー1、2のPWMは同期しない。
タイマー0は1/256処理。タイマー1,2は1/255で処理されている
ので、周期が異なるから。
・タイマー1,2でデューティー、ジャスト50%を得ようとしても、できない。
127だと、H:1.016ms + L:10.24msに。
128だと、H:1.024ms + L:1.016msになってしまう。
(タイマー0は周期1.024msだけど、タイマー1,2は2.040ms周期)
| 固定リンク
« Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ | トップページ | ダイソーReVOLTES単3 JIS C8708:2019充放電試験 400サイクル目完了 »
「Arduino」カテゴリの記事
- 1/nカウント方式とDDS方式の2相パルス発生回路(2024.10.13)
- おっと。map関数の計算桁に注意(2024.10.06)
- DDS方式の2相パルス発生回路、周波数スキャン機能を付ける(2024.10.05)
- 1クロックでも速くしたい 割込を「ISR_NAKED」で(2024.09.30)
- 1クロックでも速くしたい DDS方式の2相パルス発生器(2024.09.27)
コメント
なんで細かいことをウダウダ言うのか・・・・
この関数を「analogWrite」なんて名付けているから。
「pwmout」あたりだったら「ふ~ん」だったかも。
「アナログ」となると、1クロック、1bitの誤差でも間違いは間違い。
なんでこうなっているのかを明らかにしとかなくちゃなりません。
投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年2月12日 (水) 10時47分
※関連
・2020年8月16日:Arduino UNOのPWM出力を(ちょっと精密に)確かめる
http://igarage.cocolog-nifty.com/blog/2020/08/post-ba853b.html
・2020年8月13日:Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMを使おう
http://igarage.cocolog-nifty.com/blog/2020/08/post-0d2777.html
・2020年2月 4日:Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ
http://igarage.cocolog-nifty.com/blog/2020/02/post-c1f98a.html
・2019年12月20日:Arduino UNOで周波数カウンタ
http://igarage.cocolog-nifty.com/blog/2019/12/post-8ded09.html
投稿: 居酒屋ガレージ店主(JH3DBO) | 2020年8月16日 (日) 17時18分