1章クロック周波数とレジスタ

Arduino UNO R4のマイコン

MCU(マイコン): RENESAS社製 RA4M1 グループ,型番 R7FA4M1AB3CFM

Arduino UNO R4から見た特徴は...

です.Arduino UNO R3のマイコン AVRマイコン ATmega328Pのクロック周波数は16MHzでした.
しかし,Arduino UNO R3のA/D変換の分周比(プリスケーラ)はデフォルトでは1/128であったため,analogRead時のA/D変換のクロック周波数は125kHz(=16MHz/128)でした.
その上で,Arduino UNO R3のA/D変換には13クロック,さらにanalogReadの諸作業によって+αクロック加算された時間が1回のanalogReadにかかる時間でした.
そのために,Arduino UNO R3のA/D変換の精度を犠牲にして高速にさせる方法として,「ADPS(ADC Prescaler Select Bits)レジスタ」によって分周比を標準の1/128(=1/2^7)から変更するおまじないとして「ADCSRAレジスタ」の設定を書いたことでしょう.

では,Arduino UNO R4のマイコンRA4M1のクロック周波数48MHzはどういった意味でしょうか?
Arduino UNO R3のように精度を犠牲にしてA/D変換を高速化できるのでしょうか?

図: マイコンRA4M1の「ユーザーズマニュアルーハードウェア編」のp.142 表8.2より引用

クロック周波数は?

ルネサス社製のマイコンRA4M1の「Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編」のp.142の表8.2を見てみましょう.
内部クロックのクロック発生回路の仕様が書かれています.
様々な内部クロックがある中で,よく使いそう(設定できそう)なのは以下の6つくらいです:

この時点で既にA/D変換のクロックは最大64MHzとなっており,最大周波数48MHzよりも大きいことがわかります.
では,次に,Arduino UNO R4のデフォルトの分周比がどうなっているか,調べていきましょう!

図: マイコンRA4M1の「ユーザーズマニュアルーハードウェア編」のp.146の8.2.1項より引用

分周コントロールレジスタ(SCKDIVCR)

ルネサス社製のマイコンRA4M1の「Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編」のp.146 「8.2.1 システムクロック分周コントロールレジスタ(SCKDIVCR)」を見てみましょう.
重要なのは「アドレス」です.

特に,Arduino IDEから呼び出す際に「SYSTEM.SCKDIVCR」が関わってきます.

また,SCKDIVCRレジスタの各ビット(1要素3ビット分)で各クロックの分周比を設定できます:

また,3ビットの役割は

となっています.
さらに,マイコンRA4M1をリセットした場合の値はすべて「1  0  0」に設定されていることから,何もいじっていないマイコンRA4M1のクロックの分周比はすべて1/32であることが読み取れます.
しかし,Arduino UNO R4のデフォルトの分周比すべて1/32であるとは限りません.
そこで,Arduino UNO R4のデフォルトの分周比を調べるために,Arduino IDEからSCKDIVCRレジスタへアクセスして,分周比を確認してみましょう.

Arduino IDEからSCKDIVCRレジスタの値全体の読み取り

SCKDIVCRレジスタのアドレスは「SYSTEM.SCKDIVCR 4001 E020h」でしたね.
この「SYSTEM.SCKDIVCR」を改造していくことで,Arduino IDEからSCKDIVCRレジスタにアクセスすることができます.
すべてのレジスタで試していないので真偽はわかりませんが,きっと他のレジスタでも同じ方法で基本的にはアクセスできるはずです.

以上です!(簡単ですね~)

実際にArduino IDEでSCKDIVCRレジスタの値を確認するソースコードを書いてみましょう.

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial.print("SYSTEM.SCKDIVCR (HEX) = ");
  Serial.println(R_SYSTEM->SCKDIVCR, HEX);
}

void loop() {
  // put your main code here, to run repeatedly:
}

結果は,16進数で「10010100」でした.
これを2進数に直すと「0  001  0  000  00000001  0  000  0  001  0  000  0  000」となります.
この値に対してルネサス社製のマイコンRA4M1の「マニュアルーハードウェア」のp.146のSCKDIVCRレジスターの配列の値に書き直すと

となります.
すなわち,Arduino UNO R4のデフォルトでは最大周波数であるシステムクロック(ICLK)は48MHz,A/D変換(PCLKC)は64MHzで動作していることがわかります.
これで,「Arduino UNO R3のようにArduino UNO R4のA/D変換のクロックの分周比を変えて高速化できないの?という質問」に関する回答は,「分周比の変更によるA/D変換の高速化は不可能」と言えます.

Arduino IDEからSCKDIVCRレジスタの各要素の値の読み取り

レジスタ全体の値の読み取り方法はドット(.)をアロー(->)に変えて,先頭に「R_」を付けることでした.
ですが,毎回,全体を読み取るのは面倒です.
そのために,例えば,SYSTEM.SCKDIVCRのICK[2:0]に直接アクセスできる方法(おそらく,共用体)があります.

実際に,Arduino IDEでSCKDIVCRレジスタの各要素の値を読み取るソースコードを書いてみましょう:

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial.print("ICK (BIN) = ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.ICK, BIN);

  Serial.print("PCKA (BIN) = ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.PCKA, BIN);

  Serial.print("PCKB (BIN) = ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.PCKB, BIN);

  Serial.print("PCKC (BIN) = ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.PCKC, BIN);

  Serial.print("PCKD (BIN) = ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.PCKD, BIN);

  Serial.print("FCK (BIN) = ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.FCK, BIN);
}
void loop() {
  // put your main code here, to run repeatedly:
}

結果は

のため,もちろん,前の結果と変わりませんね.
(※2進数表記では001は1と表示され,000は0と表示されます.また,010は10と表示されます.)

もう読み込みはいいから,SCKDIVCRレジスタを変えたいんだ!ってなってきますね.
ちょっとフライングして,
R_SYSTEM->SCKDIVCR_b.PCKC = 0b010;
のようにA/D変換のクロックの分周比を1/4に変えてしまえ,ということ試した方,残念ながら,このままでは,レジスタの値を変更することができません.
プロテクトレジスタによってレジスタの値は書き換え禁止になっています.
そこで,次はプロテクトレジスタの値を書き換えて,レジスタの書き換えを許可する方法を紹介します.

図: マイコンRA4M1の「ユーザーズマニュアルーハードウェア編」のp.262 12.2.1項より引用

プロテクトレジスタ(PRCR)

ルネサス社製のマイコンRA4M1の「Renesas RA4M1グループ ユーザーズマニュアル ハードウェア編」のp.262 「12.2.1 プロテクトレジスタ(PRCR)」を見てみましょう.
まず,重要なのはアドレスです:

次に,SYSTEM.PRCRの要素は以下の4つがあります:

ということで,A/D変換のクロックの分周比を調整するPCLKC (PCKC)を書き換えたい場合,PRC0 を1に書き換える必要があります.
しかし,SYSTEM.PRCRの要素PRC0を0から1に変えるためのプログラム
R_SYSTEM->PRCR_b.PRC0 = 0b1;
を書いても書き換わりません! (->やR_,_bについては,もう大丈夫ですよね?)
その理由がPRKEYにあります.
PRKEYはPRC0, PRC1, PRC3を書き換えるための鍵であり,SYSTEM.PRCRに対して,PRKEYを含めた全16ビットを同時に書き換えなければ,PRC0, PRC1, PRC3を書き換えることができません.
書き換えたい場合,b15からb8に位置するPRKEYは16進数でA5にせよ,と仕様書には書かれています.
その上,b7からb4は2進数で0000であるため,16進数で「0xA50」までは確定しています.
最後のb3からb0の4ビットでPRC0, PRC1, PRC3のフラグを調整します.

のように,b3からb0のビットのフラグを調整すれば,プロテクトレジスタのどの要素のフラグを1にするのか指示できます.
もちろん,必要な要素のみフラグを1にして下さい.
また,必ず,使い終わったらプロテクトレジスタのフラグを0にしましょう.

実際に,Arduino IDEからプロテクトレジスタを操作するプログラム例を書いてみましょう:

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  Serial.print("Defalt: PRC0 (BIN) = ");
  Serial.println(R_SYSTEM->PRCR_b.PRC0, BIN);

  R_SYSTEM->PRCR = 0xA501;  // PRC0のみ1に変える

  Serial.print("Update? PRC0 (BIN) = ");
  Serial.println(R_SYSTEM->PRCR_b.PRC0, BIN);

  R_SYSTEM->PRCR = 0xA500;  // PRC0,PRC1,PRC3を0に変える

  Serial.print("Finish: PRC0 (BIN) = ");
  Serial.println(R_SYSTEM->PRCR_b.PRC0, BIN);
}

void loop() {
  // put your main code here, to run repeatedly:
}

この結果は下記のようになるはずです:

Defalt: PRC0 (BIN) = 0
Update? PRC0 (BIN) = 1
Finish: PRC0 (BIN) = 0

これによって,プロテクトレジスタの要素PRC0のフラグを1にして,クロック周波数の分数比を調整する準備ができました.

いよいよ,次にA/D変換のクロック周波数の分数比を変えてみましょう.

Arduino UNO R4でA/D変換のクロック周波数の分周比を変えてみよう!

ここまでちゃんと読んだ方は,「Arduino IDEにてArduino UNO R4のA/D変換のクロック周波数の分周比を1/2にするプログラムを書け」という問題を出しても,きっと解けるはずです.
もし,時間に余裕があるなら,↑を復習しながら取り組んでみましょう!




正解:
R_SYSTEM->PRCR = 0xA501;
R_SYSTEM->SCKDIVCR_b.PCKC = 0b001;
R_SYSTEM->PRCR = 0xA500;

復習も兼ねて1行ずつ解説していきましょう.

まず,
  R_SYSTEM->PRCR = 0xA501;
は,プロテクトレジスタ(PRCR)のPRC0を1にする,という意味でしたね.
特に「PRC0を1にする」というのは,「クロック発生回路関連レジスタへの書き込みを許可する」という意味でした.
これで,クロック発生回路関連レジスタへ書き込みができるようになりました.

続いて
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b001;
です.
これは,SCKDIVCRレジスタの要素PCKCに0 0 1をセットする,という意味でしたね.
「PCKCに0 0 1をセットする」というのは,「ADC14変換クロックである周辺モジュールクロックC(PCLKC)を2分周に設定する」しています.
例えば,応用として,PCLKCを0 1 0  にセットしたら,A/D変換のクロックの分数比を4分周に設定できましたね.

最後に,
  R_SYSTEM->PRCR = 0xA500;
は,プロテクトレジスタ(PRCR)のPRC0,PRC1,  PRC3をすべてを0にする,という意味でした.
すなわち,「レジスタの書き込みを禁止する」という意味になります.

実際にArduino IDEで書き換わっているか,確かめてみましょう:

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);

  Serial.print("Default PCKC: ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.PCKC, BIN);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b001;
  R_SYSTEM->PRCR = 0xA500;

  Serial.print("Update PCKC: ");
  Serial.println(R_SYSTEM->SCKDIVCR_b.PCKC, BIN);
}

void loop() {
  // put your main code here, to run repeatedly:
}

これで,A/D変換のクロックの分周比が1/2に変わったはずです.
しかし,これでは,本当に遅くなったのか実感がわきませんね...
そこで,次は,実際に分周比を変えることでanalogReadの速度が遅くなることを確認してみましょう!

A/D変換のクロックの分周比によるanalogReadの時間変化を見てみよう!

少々長いですが,まずはテスト用のプログラムを紹介します.
プログラムの後に重要な個所を説明していきます:

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
 
  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b000;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b000 (prescaler: 1, Clock: 64MHz)");
  speedtest(64);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b001;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b001 (prescaler: 2, Clock: 32MHz)");
  speedtest(32);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b010;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b010 (prescaler: 4, Clock: 16MHz)");
  speedtest(16);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b011;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b011 (prescaler: 8, Clock: 8MHz)");
  speedtest(8);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b100;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b100 (prescaler: 16, Clock: 4MHz)");
  speedtest(4);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b101;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b101 (prescaler: 32, Clock: 2MHz)");
  speedtest(2);

  R_SYSTEM->PRCR = 0xA501;
  R_SYSTEM->SCKDIVCR_b.PCKC = 0b110;
  R_SYSTEM->PRCR = 0xA500;
  Serial.println("PCKC = 0b110 (prescaler: 64, Clock: 1MHz)");
  speedtest(1);

  delay(60000);
}

void speedtest(int clock_value){
  unsigned long StartTime = millis();
  int max_iter = 1024;
  for(long i=0; i < max_iter; i++){
    int res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
    res = analogRead(A0);
  }
  unsigned long StopTime = millis();
  double analogread_time = double(StopTime - StartTime)*1000/(max_iter*20);
  Serial.print("The time per analogRead is ");
  Serial.print(analogread_time);
  Serial.println(" [μs]");

  Serial.print("Equation: ");
  Serial.print("1/48*x + 1/");
  Serial.print(clock_value);
  Serial.print("*y = ");
  Serial.println(analogread_time);
  Serial.println("");
}

まず,このプログラムには以下の3つの関数があります:

関数setup()は特に何も書かれていないため,何も実行されません.
関数loop()では,

を繰り返しています.
すなわち,関数loop内で説明していないのは関数speedtestだけ,他は読み解けるはずです.

続いて関数speedtestの中身について紹介します.
関数speedtestでは,20回のanalogReadをfor文で1024回繰り返し,そのときにかかった時間を計測しています
すなわち,20480回のanalogReadにかかった時間を計測しています.
その上で,analogReadを1回実行するにあたりかかる時間を
double(StopTime - StartTime)*1000/(max_iter*20);
で計算しています.
計測された時間に対し,「*1000」で[ms]を[μs]に変換し,20480回(= max_iter*20)で割っています.

また,for文の影響をなくすために,1回のanalogReadをfor文で20480回繰り返すプログラムにしていません.
すなわち,ループアンローリング(ループの展開)によってをfor文の終了条件の判定回数などを減らしています.
実際に,ループの展開数を1→10→20と変えていくと,若干ながら計測時間が速くなりました
それに対し,ループの展開を20→40に変えても,計測時間に影響がほぼ見られませんでした.
そのために,analogReadの計測時におけるfor文の影響はループの展開を20くらいにすれば見えなくなると推察しました.
なお,結果を代入するres=は計測時間に影響はありませんでしたが,普段,analogReadを使うことを想定して書いております.
また,関数speedtest内の
Serial.print("Equation: ");
以降に何か色々と出力させていますが,次の「1回のanalogReadにおけるクロック数は?」で説明しますので,ここでは無視をしてください.

その結果,1回のanalogReadにおける時間は次のような結果が得られました:

この結果をみると,A/D変換の分周を大きくすることで,関数analogReadの速度が遅くなることが見てとれます.

しかし,64MHzのときは21.39 [μs]だったのに対し,32MHzに設定した際は23.78 [μs]と1.116倍程度遅くなるだけで,純粋に2倍遅くなるわけではないことに気が付きます.
64MHzは1秒間に64,000,000クロックあることを意味するため,1回あたりのクロックの時間は0.015625 [μs]になります.
同様に32MHzは1秒間に32,000,000クロックあることを意味するため,1回あたりのクロックの時間は0.03125[μs]になります.
analogReadはアナログ信号をディジタル信号に変換するA/D変換を行うはずなので,A/D変換の分周を2倍大きくしたら,単純に2倍近く遅くなるはずだ,と思いたいですが,そうはなっていません.

感の鋭い人は,もうお気づきでしょう.
クロック周波数は?」で紹介したように,マイコンRA4M1には様々なクロックがありました.
時間計測しているところは,あくまで,analogRead(と影響少なくしたfor文)のみのため,その他の周辺クロックであるPCLKA, PCLKB, PCLKD, FCLKは影響がないと仮定ます.
今回,分周比を変更したのはADC14変換クロックであるPCLKCのみで,システムクロックICLKはデフォルトの48MHzの設定のままです.
そのとき,analogReadの主な時間はA/D変換ではなく,システムクロックICLKが扱う計算ではないか?と推察できます.
そこで,本章の最後として,1回のanalogReadにおける「A/D変換用クロックPCLKCのクロック数」と「システムクロックICLKのクロック数」を推察してみましょう!

図. analogReadのクロック数の推察

1回のanalogReadにおけるクロック数を推察しよう!

ここでも前節と同様にArduino UNO R4のArduino APIのanalogReadでは,シリアル通信やDACなどは行われていない,すなわち,analogReadではPCLKA, PCLKB, PCLKD, FCLKは影響がないと仮定して話を進めます.
本節では前節の測定結果を用いて,2変数(x, y)の連立方程式に帰着し,クロック数を推察します.(特に,前節で一旦忘れてもらったEquationに関係します!)

まず,1回のanalogReadにかかる時間

システムクロック(ICLK)による演算時間 + A/D変換のクロック(PCLKC)による演算時間

と書けます.
次に未知変数として

とします.
そのとき,

システムクロック(ICLK)を用いた演算時間 = 1クロックの時間*x

となります.
さらにシステムクロック(ICLK)は48MHzで固定しているため1クロックあたりの時間[μs]は1/48( = 1/48000000*1000*1000)  となります.
よって,

システムクロック(ICLK)を用いた演算時間[μs] = 1/48[μs] * x[回] 

となります.

同様に,「A/D変換のクロック(PCLKC)を用いた演算時間」も考えていきます.
但し,A/D変換のクロック(PCLKC)は分周比を変化させているため,

とします.
すなわち,PCLKCを1分周にしたときはf=64 [MHz]を意味し,2分周にしたときはf=32[MHz]を意味します.
このとき,A/D変換のクロック(PCLKC)を利用した場合の1クロックあたりの時間[μs]は1/f [μs] (= 1/(f*1000*1000)*1000*1000)となります.
よって,

A/D変換のクロック(PCLKC)を用いた演算時間[μs] = 1/f[μs] * y[回] 

となります.

以上から,PCLKCをn分周にしたとき,方程式

1/48[μs] * x[回]  + 1/f[μs] * y[回] =  1回のanalogReadにかかる時間[μs]

が得られます.

例えば,PCLKCを1分周(f=64MHz)にしたとき,前節の測定結果から1回のanalogReadにかかる時間21.39 [μs] であったことを利用すると

1/48[μs] * x[回]  + 1/64[μs] * y[回] = 21.39 [μs]

という方程式になります.

以上を他の分周比の場合もまとめると7つの方程式

を得られます.
これが,前節の結果で表示していたEquationの意味です.
未知変数が2変数であるため,上記の内,任意の2つ方程式を選んで連立方程式として解けば,「システムクロック(ICLK)を用いたクロック数x」と「A/D変換のクロック(PCLKC)を用いたクロック数y」が推察できます.

方程式の数が7つあり,そこから2つ組み合わせを選ぶため,全部で21通りあります.
21通りに対し,解は得るプログラムをMatlabで書くと下記のようになります:

A_all = [
    1/48, 1/64;
    1/48, 1/32;
    1/48, 1/16;
    1/48, 1/8;
    1/48, 1/4;
    1/48, 1/2;
    1/48, 1/1;
    ];

b_all = [
    21.39;
    23.78;
    28.37;
    37.35;
    55.42;
    91.41;
    164.06;
    ];

A = zeros(2,2);
b = zeros(2,1);
res = zeros(2,15);

k = 1;
for i = 1:6
    for j = i+1:7
        A(1,:) = A_all(i,:);
        A(2,:) = A_all(j,:);
        b(1) = b_all(i);
        b(2) = b_all(j);
        x = A\b;
        res(:,k) = x;
        k = k + 1;
    end
end

上記に対し,箱ひげ図は

boxchart(res(1,:)')

boxchart(res(2,:)')

で書くことができます.
実際に全てのパターンで解いた解は下記のようになります:

上記の結果や「図. analogReadのクロック数の推察」から

くらいなのではないか?と推察することができます.(あくまで,推察であり,正しさは保証されていませんのでご注意ください.)

この結果から,デフォルト (ICLK: 48MHz, PCLKC: 64MHz) のanalogReadの1回あたりの時間の割り振りは,

くらいと計算できます.

このことから,Arduino APIのanalogReadはA/D変換にかかる時間よりも,ほかの処理にかかる時間が主要なのではないか?と考えられます
そこで,次はanalogReadのソースコードを解析し,analogReadでシステムクロックを使う時間を減らし,A/D変換に特化させる方法を考えていきましょう!