asknのアイコン画像
askn 2026年09月06日作成 © MIT
セットアップや使用方法 セットアップや使用方法 Lチカ Lチカ 閲覧数 43
askn 2026年09月06日作成 © MIT セットアップや使用方法 セットアップや使用方法 Lチカ Lチカ 閲覧数 43

[modernAVR] USARTボーレート周波数をセルフ観測する

[modernAVR] USARTボーレート周波数をセルフ観測する

LED_BUILTIN先が逆極性なヤツは偶に困る

Arduino互換機系の設計だと、LEDは普通"正極性"で光らせるようになっている。しかし世の中そんな設計ばかりとは限らない。

pinMode(LED_BUILTIN, OUTPUT); /* これだけでLEDが光るのが負極性 */ digitalWrite(LED_BUILTIN, HIGH); /* これでようやく光るのが正極性 */

Lチカするのならどっちでも良いんですよ。どうせ時系列で切り替わるから。

しかしHIGHLOWの使い分けを常に一定に保ちたいなら、適宜 GPIOの入出力自体を"反転"させる。

/* 負極性と判明している場合のみ、次行を実行 */ 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 の実体物理ポートが限定されているという前提はあるものの、こんなやつ。

/* 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 カウンターを使って、こんなコードで可視化してみた。

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が下限値とされる。

/* 非同期標準速 */ 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を減じる。そしてゼロ設定も許される。それゆえ式の途中で四捨五入する必要はない。

/* 非同期標準速 */ 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=24000000LCONSOLE_BAUD=500000L とした場合、1周期あたり 標準速では 6 CLK、倍速では 12 CLK になる筈だ。

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 レジスタへ書く値にできる。

関連リンク

asknのアイコン画像
朝日薫(通名)◆KEEP TRYING, THE GOD SAVE YOU!◆悩んでる暇があったら手を動かせ◆本質的にはデバグ解析修理屋◆ソフトもハードも全部やる◆開発言語だけなら数十種は読み書き可◆最初に覚えた言語はZ80ニモニックの手書き◆主力はPerl◆
  • askn さんが 前の日曜日の20:05 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する