« Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ | トップページ | ダイソーReVOLTES単3 JIS C8708:2019充放電試験 400サイクル目完了 »

2020年2月10日 (月)

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変換、こんな回路を使ってます。
B1_20200210161601
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」カテゴリの記事

コメント

なんで細かいことをウダウダ言うのか・・・・
この関数を「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分

コメントを書く



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




« Arduinoのタイマー OCRレジスタは「n」じゃなく「n - 1」の値を設定せよ | トップページ | ダイソーReVOLTES単3 JIS C8708:2019充放電試験 400サイクル目完了 »