UART割り込み受信の実装
UART通信では、受信データが届くタイミングは送信側しだいです。本記事では、割り込みハンドラ(ISR:Interrupt Service Routine)とリングバッファの設計、オーバーラン対策、可変長データの受信を解説します。
なぜUART受信は割り込みで実装するのか
CPUが他の処理を抱えていると、受信データの読み出しが間に合わず、取りこぼし(データ欠落)が起こるおそれがあります。取りこぼしたデータは受信処理だけでは復元できないため、発生を抑える設計が必要です。
受信の実装方式には、レジスタを繰り返し確認する「ポーリング」、データ到着時だけ処理を起動する「割り込み」、CPUを介さずメモリへ転送する「DMA(Direct Memory Access)」の3つがあります。
ポーリングは実装が簡単な一方で、CPU時間を消費しやすいです。確認の間隔が長かったり、他の処理で読み出しが遅れたりした場合に取りこぼしが起こりやすいです。
割り込みはCPUを有効に使いながら取りこぼしを防ぎやすく、多くの組み込みシステムで広く使われています。割り込み受信の処理はUARTと割り込みの初期化、受信割り込みの許可、ISRでのデータ取得、リングバッファへの格納、メイン処理での取り出しという流れで構成されます。
DMAは高速・大量の受信に強いものの、設定が複雑で機種への依存もあります。
各方式の比較をまとめると、次のようになります。
| 方式 | 長所 | 短所 |
|---|---|---|
| ポーリング | 実装が容易 | CPU時間消費 |
| 割り込み | CPU効率が良い、取りこぼし防止 | 実装複雑度中 |
| DMA | 高速・大量の受信に強い | 機種依存大、設定複雑 |
ISRは短く保つ
ISRの役割は、受信データレジスタ(RDR/DR)からのデータ読み出しと、バッファへの格納です。データの解析や応答の組み立てといった重い処理をISR内でおこなうと、次のデータの受信に間に合わず取りこぼしの原因になります。解析の処理はメイン処理へ渡すのがよいでしょう。
一部のマイコン向けHAL(Hardware Abstraction Layer)ライブラリには、指定バイト数を受信すると割り込みを自動停止する仕様のものがあります。そのようなライブラリでは、受信完了コールバック内で受信関数を呼びなおし、割り込みを再登録します。使用するHAL/APIの仕様は事前に確認しておくとよいでしょう。
ISRとメイン処理で共有する変数には、volatile修飾子を付けるのが一般的です。読み書きの位置を示すheadやtail、受信完了フラグにvolatileを付けないと、コンパイラの最適化によって変数の変化が正しく読み取れないケースがあります。ただし、volatileは排他制御まで保証するものではないため、複数バイトの共有データは、必要最小限の区間だけ割り込みを禁止する方法が適切です。
キャッシュを持たないシングルコアマイコンではvolatileで十分な場合がほとんどですが、キャッシュ付きアーキテクチャでは不十分な場合があります。そのような場合は、_Atomic型や適切なメモリバリアの使用も検討するとよいでしょう。
受信データはリングバッファで受け止める
ISRが受け取ったデータは、リングバッファと呼ばれる円環状のバッファへためる方法が一般的です。ISRがためたデータをメイン処理が順に取り出すことで、受信速度と処理速度の差を吸収できます。受信が一時的に集中しても、バッファに空きが有るあいだは取りこぼしのリスクを抑えられます。
バッファサイズは一度に届くデータのバースト量と、メイン処理が取り出す周期から見積もります。サイズを2の累乗にしておくと、格納位置のラップアラウンド(折り返し)計算を割り算の余りではなく、サイズから1を引いたマスク値とのビット演算(AND)に置き換えられ、ISR内の処理時間を短縮できます。※index=(index+1)&(size-1)のように設計できる場合です。
読み書きポインタで配列の領域を再利用する
リングバッファは書き込み位置を示すheadと、読み出し位置を示すtailの2つのポインタで管理します。headはISRがデータを格納するたびに、tailはメイン処理が取り出すたびに進み、配列の末尾に達したら先頭へ戻ります。この動きによって、固定長の配列を循環的に再利用できます。
注意したいのはheadとtailが一致した状態では、空と満杯を区別できない点です。区別する方法は「1要素を常に未使用にする」、「格納されているデータ数を別の変数で管理する」、「満杯フラグを設ける」の3つです。方法ごとに特徴が異なるため、実装前に使用する方法を決めておくとよいでしょう。
満杯時はデータの破棄か上書きかをあらかじめ決める
バッファが満杯のときに新しいデータが届いた場合の動作も、設計時にあらかじめ決めておきます。選択肢は新しいデータを破棄するか、最も古いデータに上書きするかの2つです。最新の値だけが重要なセンサ値の受信では上書き、取りこぼしを検出したい通信では破棄が向いています。
どちらを選ぶ場合でも破棄や上書きが発生した回数を記録しておくと、取りこぼしの発生に後から気付けます。記録した回数は、バッファサイズを見なおす際の判断材料にもなります。なお空きができるまでISR内で待機する方式は、他の割り込みを妨げてシステム全体の停止につながるおそれがあるため、注意が必要です。
オーバーラン発生時は解除と再同期までおこなう
受信データレジスタを読み出さないまま次のデータが届くと、オーバーランエラー(ORE:Overrun Error)が発生します。ISRの実行が他の割り込みや割り込み禁止区間により遅れた場合のほか、ポーリング方式での読み出し遅延など、読み出しが間に合わない状況全般が原因となります。
対策の基本はISRを短く保つことと、UART受信割り込みの優先度を上げることです。それでも解消しない場合は、DMAの併用やボーレートの見なおしも検討するとよいでしょう。
注意点は、ORE発生後の挙動や解除の手順がマイコンによって異なることです。代表例として、ステータスレジスタを読んでから受信データレジスタを読み出してクリアする系統や、ICRなどのクリア用レジスタへ所定のビットを書き込んでクリアする系統があります。クリアしただけでは受信が再開されない機種も有るため、仕様で規定された読み出し順序まで含めて、データシートやリファレンスマニュアルの手順に従って再開させます。
さらにエラー発生前後のデータは途中から欠けている可能性があるため、フレームの区切りを検出して同期を取りなおす再同期の処理も必要です。
なおフレーミングエラー(FE)・パリティエラー(PE)・ノイズエラー(NE)が発生した場合の処理も、マイコンの仕様にあわせて決めます。多くの場合は、該当データを破棄して受信を継続する設計が候補になります。
可変長データを区切り、受信処理をタスクへ渡す
受信するフレームの長さが決まっていない場合は、データの区切りを検出する仕組みが必要です。主な方法は「改行コードの終端文字で区切る」、「フレームの先頭で長さを指定する」、「受信が途切れたことを示すIDLE割り込みで検出する」の3つです。IDLE割り込みを使えるのはIDLEライン検出機能を備えたマイコンで、フレーム間に一定のギャップが空くことが通信仕様で保証されている場合に限られます。
なお、IDLEフラグのクリア手順はマイコン依存です。手順を誤ると割り込みが再発し続けるため、リファレンスマニュアルに規定された手順に従う必要があります。どの方法を使えるかは相手側の送信仕様にも左右されるため、通信仕様の確認が先になります。
RTOS(Real-Time Operating System)では、区切ったデータをバッファやキューでタスクへ渡します。「バイト列はバッファ」、「フレーム単位はキュー」、「可変長メッセージは単位ごとに扱える仕組み」が適切です。実装によっては単一の書き込み元と読み出し元を前提とするため、書き込み元が複数の場合は経路をまとめるか、対応する仕組みが必要です。ISRから渡す際は、利用するRTOSの割り込み専用APIを使用します。
割り込みによるUART受信は「ISRの短さ」、「リングバッファの設計」、「エラー処理」の3つがそろって安定します。取りこぼしを抑えた受信処理が、機器全体の信頼性を支える基盤となります。上述の考え方は、UARTに限らず他のシリアル通信の受信処理にも同じように応用できます。


