askn が 2026年09月06日20時05分50秒 に編集
初版
タイトルの変更
[modernAVR] USARTボーレート周波数をセルフ観測する
タグの変更
modenAVR
AVR64DU32
AVR64DD32
メイン画像の変更
記事種類の変更
セットアップや使用方法
Lチカの変更
ライセンスの変更
(MIT) The MIT License
本文の変更
## LED_BUILTIN先が逆極性なヤツは偶に困る Arduino互換機系の設計だと、LEDは普通"正極性"で光らせるようになっている。しかし世の中そんな設計ばかりとは限らない。 ```c pinMode(LED_BUILTIN, OUTPUT); /* これだけでLEDが光るのが負極性 */ digitalWrite(LED_BUILTIN, HIGH); /* これでようやく光るのが正極性 */ ``` Lチカするのならどっちでも良いんですよ。どうせ時系列で切り替わるから。 しかし`HIGH`と`LOW`の使い分けを常に一定に保ちたいなら、適宜 GPIOの入出力自体を"反転"させる。 ```c /* 負極性と判明している場合のみ、次行を実行 */ pinControlRegister(LED_BUILTIN) = PORT_INVEN_bm; /* MacroAPI での記法 */ digitalWrite(LED_BUILTIN, HIGH); /* HIGHで必ず点灯できる */ ``` ちなみに "Curiosity" や "XPlained" はだいたい”負極性”だ。正直どっちかに統一する決まりがある訳でもない。まあ回路設計する側面から見ると"負極性"のほうが GPIOに 数十mA 以上の大電流が流れても、マイコン自身の VCC(VDD) へ及ぼす電圧変動が少ないので、安定性向上に少しは寄与できる。GNDプレーン設計がガッシリしてる前提だけれど。 ++AVR-Dx系の電力設計は、VCC で +350mA、GPIO 1本あたり ±50mA だ。なので "吐き出し" だと 6本までしか MAX で吐けないが、"吸い込み" はこれを上回ることができる。++ それで。ちょっと悩むのは作り捨ての応用記述で、とりあえず MPUが起動した(暴走も brickもしてない)ことを示すため、単に LED_BUILTIN を光らせたいケース。かつ同じ応用記述を複数機種で使い回す場合。公開サンプルスケッチなぞ特にそうですな。ちゃんと対応するならタイマー割込をコピペすれば良いのだが、いろいろやっててタイマーの空きがない場合や、割込が邪魔になる場合は、悩ましい。 それで最近使ってるコードは、まあある程度 LED_BUILTIN の実体物理ポートが限定されているという前提はあるものの、こんなやつ。 ```c /* LED が PF2 の場合 */ EVSYS_CHANNEL0 = EVSYS_CHANNEL_USART0_XCK_gc; EVSYS_USEREVSYSEVOUTF = EVSYS_USER_CHANNEL0_gc; /* EVOUTF: PF2 */ /* LED が PA7 の場合 (DxCoreやMegaCore使ってるならこれ一択) */ EVSYS_CHANNEL0 = EVSYS_CHANNEL_USART0_XCK_gc; PORTMUX_EVSYSROUTEA = PORTMUX_EVOUTA_ALT1_gc; /* EVOUTA: PA7 */ EVSYS_USEREVSYSEVOUTA = EVSYS_USER_CHANNEL0_gc; ``` `EVSYS_CHANNEL_USARTn_XCK_gc`には、対応する USART 周辺機能が生きているなら、その設定ボーレートに応じた周波数の信号(HIGH/LOW)が常時出力されている。なので LED_BUILTIN 実体が `EVOUTx` と合致するなら、EVSYS(と必要ならPORTMUX)経由で信号分配してしまえば、Duty 比 50 % で明るさ 1/2 の LED点灯ができる。しかも特別なタイマーを別途用意する必要もない。`Serial`活動中なら USART が動いていないなんてこともありえない。これが目に見えて点滅してるなら WDT が効いてるとかの無限リセットループしてるのだとわかる。 しかしデータシートを読むと`EVSYS_CHANNEL_USARTn_XCK_gc`は、USARTが"同期モード"か"SPI主装置モード"の時のみ有効と書かれているが、なんで信号が出てるんだコレ? また裏技見つけちまったか・・・懲りないな。 ++書いてないということは、あらゆる姉妹品種で同様に動作するかは不明ということ。++ ## USARTボーレート周波数をセルフ観測する 一応これだけでも終わる話だが、USART を吐いている MPU自身で、その周波数をタイマー/カウンター測定によってセルフ検証することができる。`USARTn_BAUD`レジスタを読んで逆算すれば済むことでもあるのだが、計算式が`F_CPU`値に依存するのが面白くないし、LIN通信のような自動ボーレート設定されるモードを使ってると余計に現在値がわからなくもある。 それで。主クロックと独立で 32.768 kHz基準の周期イベントを吐ける PIT機能と、16-bit TCB0 カウンターを使って、こんなコードで可視化してみた。 ```c void setup (void) { /* 外部 Xtal 32.768 kHz 発振器が回路にあるなら、この2行を有効にする */ _PROTECTED_WRITE(CLKCTRL_XOSC32KCTRLA, CLKCTRL_ENABLE_bm | CLKCTRL_RUNSTDBY_bm); loop_until_bit_is_set(CLKCTRL_MCLKSTATUS, CLKCTRL_XOSC32KS_bp); /* 32.768 kHz 発振器で、高周波主発振器を自動校正する : AVR-DU の場合 */ uint8_t _reg = (CLKCTRL_OSCHFCTRLA & ~CLKCTRL_AUTOTUNE_gm) | CLKCTRL_AUTOTUNE_32K_gc; _PROTECTED_WRITE(CLKCTRL_OSCHFCTRLA, _reg); loop_until_bit_is_set(CLKCTRL_MCLKSTATUS, CLKCTRL_OSCHFS_bp); /* Serial == USART1 として、PF2(LED)に USART1_XCLKを送る */ EVSYS_CHANNEL0 = EVSYS_CHANNEL_USART1_XCK_gc; EVSYS_USEREVSYSEVOUTF = EVSYS_USER_CHANNEL0_gc; /* 同時に TCB0 で、それをカウントする */ EVSYS_USERTCB0COUNT = EVSYS_USER_CHANNEL0_gc; /* CH1で、イベントを捕獲できるようにする */ EVSYS_CHANNEL1 = EVSYS_CHANNEL_RTC_EVGEN0_gc; EVSYS_USERTCB0CAPT = EVSYS_USER_CHANNEL1_gc; /* TCB0 で、USART1_XCLKをカウントする */ TCB0_EVCTRL = TCB_CAPTEI_bm; TCB0_CTRLB = TCB_CNTMODE_FRQ_gc; TCB0_CTRLA = TCB_ENABLE_bm | TCB_CLKSEL_EVENT_gc; /* PIT周期イベント 32Hz (32768/1024) を CH1 に送る */ /* これは AVR-DU/EB 以降の書き方(EVSYS世代 2.2)*/ RTC_PITEVGENCTRLA = RTC_EVGEN0SEL_DIV1024_gc; RTC_PITCTRLA = RTC_PITEN_bm; delay_millis(1000); /* Curiosity の VCP-CDC の起動が遅いので… */ Serial.begin(CONSOLE_BAUD); while (Serial.available()) Serial.read(); Serial.println(F("\n<startup>")); Serial.print(F("F_CPU=")).println(F_CPU, DEC); Serial.print(F("_AVR_IOXXX_H_=")).println(_AVR_IOXXX_H_); Serial.print(F("CONSOLE_BAUD=")).println(CONSOLE_BAUD, DEC); Serial.flush(); } void loop (void) { /* 32Hz でキャプチャしてるので、1Hz精度に直すには 32倍して 8(倍速モードなら4)で割る */ const static uint8_t _dbl = ((USART0_CTRLB & USART_RXMODE_gm) == USART_RXMODE_CLK2X_gc) ? 4 : 8; delay_millis(1000); uint32_t _ccmp = TCB0_CCMP; Serial.print(F("BAUD=")).print(_ccmp, DEC, 6).write(','); Serial.print(_ccmp * _dbl, DEC, 7).write(','); Serial.print((float)_ccmp * _dbl / CONSOLE_BAUD * 100, 2, 7); Serial.println(" %"); Serial.flush(); } ``` (コードをきっちり読んだ諸兄なら)**ん?その係数は何?**(と思うだろうが、出力結果は以下) ``` <startup> F_CPU=20000000 _AVR_IOXXX_H_=ioavr64du32.h CONSOLE_BAUD=500000 BAUD= 61861, 494888, 98.98 % BAUD= 61858, 494864, 98.97 % BAUD= 61856, 494848, 98.97 % BAUD= 61848, 494784, 98.96 % BAUD= 61857, 494856, 98.97 % BAUD= 61846, 494768, 98.95 % BAUD= 61851, 494808, 98.96 % BAUD= 61852, 494816, 98.96 % ``` 非同期設定の USARTn が`EVSYS_CHANNEL_USARTn_XCK_gc`に通じて吐いてる周波数は、なんと設定ボーレートの 8倍なのだった。倍速モード(`USART1_BAUD <<= 1; USART1_CTRLB |= USART_RXMODE_CLK2X_gc`)であるなら 4倍に一致する。それでここでは 16-bitカウンターが ボーレート 500 kbpsまでは溢れないよう、キャプチャ周期を調整し 32 Hz で観測している。 32.768 kHz 発振器で、内蔵高周波主発振器を校正しても/しなくても、1 % 以上の誤差が観測されるとは予想外だったけど…んん? ++データシートの USARTn_BAUD の説明を読むと、非同期 USARTへは RxD 多数決サンプリングのために ボーレートの 8ないし16倍の内部クロックを要しているとある。ここで観測された周波数はその説明と矛盾しない。そしてまた `F_CPU/8` が設定可能な最速ボーレートであることも、ここから導くことができる。++ ## ちょっと待てよ? サンプリング周波数を 32 Hz としている時点で多少の精度落ちはありうる。だが 32-bit カウンターで 1 Hz キャプチャとしても有意差はなかった。しかし、自動校正を有効にしていてなお、1 % 以上のズレはどうも納得いかない。 modernAVR 系の `USARTn_BAUD` の設定値は、どの型番のデーターシートでも、次のような計算式で書かれている。二進固定小数部 6 bit、整数部 10 bit なので、64が下限値とされる。 ```c /* 非同期標準速 */ USARTn_BAUD = (64 * F_CPU) / (16 * CONSOLE_BAUD); /* min limit 64 */ /* 非同期倍速 */ USARTn_BAUD = (64 * F_CPU) / (8 * CONSOLE_BAUD); /* min limit 64 */ /* 同期主装置 */ USARTn_BAUD = ((F_CPU) / (2 * CONSOLE_BAUD)) << 6; /* min limit 64 */ ``` 一方、classicAVR / reduceAVR 系では、整数部 12 bitだ。これらでは最後に`1`を減じる。そしてゼロ設定も許される。それゆえ式の途中で四捨五入する必要はない。 ```c /* 非同期標準速 */ UBBR0 = (F_CPU) / (16 * CONSOLE_BAUD) - 1; /* 非同期倍速 */ UBBR0 = (F_CPU) / (8 * CONSOLE_BAUD) - 1; /* 同期主装置 */ UBBR0 = (F_CPU) / (2 * CONSOLE_BAUD) - 1; ``` 観測された XCLK周波数からすると、modernAVR であっても、USARTn_BAUD レジスタの実体は "分周器設定" ではなく "タイマーカウンターの溢れ設定値" と見做したほうが自然だ。倍速設定ではそのタイマー周期の全体で、標準速では 1回飛ばしの半数回で RxDサンプリングを行っていると考えれば、classicAVR / reduceAVR 世代と同じメカニズムになる推測できる。だったら、USARTn_BAUD 設定値は、BOTTOM=0 から MAX までが 1周期の筈なので; **`1`を減じるのが正解なんじゃないか?** ・・・やってみた。 ``` <startup> F_CPU=20000000 _AVR_IOXXX_H_=ioavr64du32.h CONSOLE_BAUD=500000 BAUD= 62267, 498136, 99.63 % BAUD= 62276, 498208, 99.64 % BAUD= 62273, 498184, 99.64 % BAUD= 62268, 498144, 99.63 % BAUD= 62265, 498120, 99.62 % BAUD= 62253, 498024, 99.60 % BAUD= 62256, 498048, 99.61 % BAUD= 62270, 498160, 99.63 % ``` 念の為、2を減じたパターンも観測してみる。 ``` <startup> F_CPU=20000000 _AVR_IOXXX_H_=ioavr64du32.h CONSOLE_BAUD=500000 BAUD= 62654, 501232, 100.25 % BAUD= 62643, 501144, 100.23 % BAUD= 62645, 501160, 100.23 % BAUD= 62645, 501160, 100.23 % BAUD= 62652, 501216, 100.24 % BAUD= 62626, 501008, 100.20 % BAUD= 62641, 501128, 100.23 % BAUD= 62635, 501080, 100.22 % ``` **うわあ・・・** `-2`のほうが理想値に近いのも意外だが、32.768kHz 計時発信器での測定誤差(F_CPUとCONSOLE_BAUDの両方を割り切れるか否かと割込発生間隔のブレ)を見越しても `-1`で 0.4 % 程度の誤差に収まってくる。 データシートの計算式は誤記載じゃないのか、コレ。 ## USART 分数ボーレート発生器とは つまり、推測するとこういう構造なのだと思う - USART 内蔵タイマーは`32 * F_CPU / (USARTn_BAUD + 1)`Hz の PWM(Duty1:1)周波数を発生させ、XCLKへ送っている。これは `8 * CONSOLE_BAUD` に一致する。倍速なら`USARTn_BAUD`には 2倍の数値をセットする。なので発生周波数は半分になり、 `4 * CONSOLE_BAUD`に一致する。 - これで 8-bit 相当のリングカウンタを回し、途中のビットタップをフリップフロップに繋いで、RxD 信号線の論理値を調べる回路なのだろう。 これを確認するためのコードは次のようになる。今度は`EVSYS_CHANNEL_USARTn_XCK_gc ` をキャプチャ信号として、直接 CLK_PER(F_CPU)の所要カウント数を得てみる。 `F_CPU=24000000L` で `CONSOLE_BAUD=500000L` とした場合、1周期あたり 標準速では 6 CLK、倍速では 12 CLK になる筈だ。 ```c void setup (void) { /* Serial == USART0 として、XCLKを CH0 に送る */ /* 等速動作の TCB0 で、それを捕獲する */ EVSYS_CHANNEL0 = EVSYS_CHANNEL_USART0_XCK_gc; EVSYS_USERTCB0CAPT = EVSYS_USER_CHANNEL0_gc; TCB0_EVCTRL = TCB_CAPTEI_bm; TCB0_CTRLB = TCB_CNTMODE_FRQ_gc; TCB0_CTRLA = TCB_ENABLE_bm | TCB_CLKSEL_DIV1_gc; delay_millis(1000); Serial.begin(CONSOLE_BAUD); #if 0 /* 標準速から倍速モードに再設定するなら 1 */ USART0_BAUD = ((USART0_BAUD + 1) << 1) - 1; USART0_CTRLB |= USART_RXMODE_CLK2X_gc; #endif Serial.println(F("\n<startup>")); Serial.print(F("F_CPU=")).println(F_CPU, DEC); Serial.print(F("_AVR_IOXXX_H_=")).println(_AVR_IOXXX_H_); Serial.print(F("USART_BAUD=")).println(USART0_BAUD, DEC); Serial.print(F("RXMODE_CLK2X=")).println((bool)(USART0_CTRLB & USART_RXMODE_gm), DEC); Serial.print(F("CONSOLE_BAUD=")).println(CONSOLE_BAUD, DEC); Serial.flush(); } void loop (void) { delay_millis(1000); uint32_t _ccmp = TCB0_CCMP + 1; uint16_t _baud = USART0_BAUD + 1; Serial.print(F("CAPT=")).print(_ccmp, DEC, 6).write(','); Serial.print(_ccmp * 32, DEC, 6).write(','); Serial.print(((double)_ccmp * 32 * 100) / _baud, 2, 7); Serial.println(" %"); Serial.flush(); } ``` レジスタ設定値はいずれも普通のタイマーと同じであるとするなら、レジスタ値を読んで周波数に変換する前には`+1`で補正し、レジスタへ再設定する際には`-1`とするルールに従う。 標準速の測定結果; ``` <startup> F_CPU=24000000 _AVR_IOXXX_H_=ioavr64dd32.h USART_BAUD=191 RXMODE_CLK2X=0 CONSOLE_BAUD=500000 CAPT= 6, 192, 100.00 % CAPT= 6, 192, 100.00 % CAPT= 6, 192, 100.00 % CAPT= 6, 192, 100.00 % CAPT= 6, 192, 100.00 % CAPT= 6, 192, 100.00 % CAPT= 5, 160, 83.33 % CAPT= 6, 192, 100.00 % ``` 倍速の測定結果; ``` <startup> F_CPU=24000000 _AVR_IOXXX_H_=ioavr64dd32.h USART_BAUD=383 RXMODE_CLK2X=1 CONSOLE_BAUD=500000 CAPT= 12, 384, 100.00 % CAPT= 12, 384, 100.00 % CAPT= 12, 384, 100.00 % CAPT= 12, 384, 100.00 % CAPT= 11, 352, 91.67 % CAPT= 12, 384, 100.00 % CAPT= 12, 384, 100.00 % CAPT= 12, 384, 100.00 % CAPT= 12, 384, 100.00 % ``` ぴったり。たまに 1 CLK 少ない値が捕獲されて、そこで流れが乱れるが・・・32.768 Hz 基準観測値が下振れ気味だったのはこれが理由かな。 ## まとめ なんだか最後は、予想外の方向へ深掘りしてくことになってしまったな。ほんとに「Lチカ」話かこれ? EVOUT+LED 設定ができる回路設計の場合、LED_BUILTIN の物理ポートにオシロなりデジマルなりを繋いで周波数を測定し、8 で割れば、稼働中の USARTボーレートが一発でわかる。これはこれで嬉しい。しばらく放置していてモノの設定値がわからなくなって、ターミナルのボーレート値を闇雲に設定変更するような煩わしさからも解放される。特に意味もなく LED を点灯させとくだけよりも、よほど便利で役に立つだろう。消費電力だって点けっぱなしの半分だ。かわりに余計な ECM ノイズ発生源にもなりうるのだが。 ++LEDに繋ぐのではなく、UART対抗先の別のMPUで、この出力の周波数測定を行えると正確な UART同期速度を得ることができる。MPUが modernAVR 系統なら、単に 32パルスの所用経過数を自身の CLK_PER でカウントすれば、それがそのまま USARTn_BAUDレジスタへの設定値だ。それがもし 32以上 64未満なら、2倍して、`USART_RXMODE_CLK2X_gc`フラグも立てればよい。classicAVR でなら分数発生器ではないので、4パルスをカウントして 16で割れば UBBRn レジスタへ書く値にできる。++ ## 関連リンク - [MultiX Zinnia Product SDK [modernAVR]](https://github.com/askn37/multix-zinnia-sdk-modernAVR)