割り込みの排他処理に関するバグ

TOPPERS/JSP for BlackfinのRelease 3.0.0に関して、いくつか重大なバグの報告がありました。そのうち2件にかんしては検討の結果バグであることが判明しています。以下、概要と考察です。

チケット #16714 多重割り込み時にIMASKを変更すると、間違ったハンドラが呼ばれる

現在の実装では、Blackfinプロセッサが割り込みを認識すると、cpu_support.Sのinterrupt_dispatcherラベルへと飛びます。そして、interrupt_dispatcherの中から、チップ依存部にある chip_config.cのdevice_dispatcher()が呼び出され、割り込みレベルと要求状態から逆算して、割り込みを起こしたデバイスを判定します。その判定部分のコードを以下に示します。

/*
 *  割り込みをデバイスに割り当てる
 */
void device_dispatcher( unsigned int priority )
{
	unsigned int candidates, device;
	
	candidates = priority_mask[priority] & *pSIC_ISR & *pSIC_IMASK;	// 現在のプライオリティに相当する割込み源を特定する

	if ( ! candidates ) // 割り込み源が特定できないなら、コア由来である

candidateの値を計算している部分が、割り込みを起こしたデバイスを割り出すコードです。
問題は、関数呼び出し時からcandidates変数への代入の間に割り込みがネストした場合に発生し得ます。仮にネストした割り込みハンドラがネストされた割り込みに対応するSIC_IMASKをクリアした場合*1、ネストから戻ってきたときにcandidatesの値を正しく計算できません。その結果、上記コードに続く部分で、誤ってソフトウェア割り込みハンドラを呼び出してしまうことになります。

チケット #16729 dis_int()が排他制御されていない

http://sourceforge.jp/ticket/browse.php?group_id=1240&tid=16729
dis_intはSIC_IMASKをread_modify_writeしますが、割り込み禁止にしていないため、dis_int()内部で割り込まれてSIC_IMASKが変更されると、SIC_IMASKが正しく変更されないという問題が発生します。
同じ問題はena_int()にも存在します。

ITRONTOPPERS/JSPの要求

かつてMLで質問した際には、TOPPERS/JSPに関しては割り込みネスティングの対応は必須ではないとコメントをもらっています。

また、上記問題を指摘してくださった方から、ena_int/dis_intはITRON4のスタンダード・プロファイルの外にあるという指摘ももらっています。さらに、別の方からはTOPPERS/JSPではena_int/dis_intは非タスク・コンテキストではE_CTXを返してもよいというコメントをもらっています。

そもそもSIC_IMASKのクリアは危険

ADSP-BF533のプログラミング・リファレンスを読むと、「SIC_IMASKはリセット後の諸一家時に割り込みを許可する前に設定すべきだ」、つまり、やや遠まわしにSIC_IMASKを実アプリケーションの動作中に変更するなと書いてあります。

Although this register can be read from or written to at any time (in Supervisor mode), it should be configured in the reset initialization sequence before enabling interrupts. [Blackfin Processor Programming Reference Rev 1.3, p4-39]

そうすべき理由は書いていないのですが、過去に行った実験から想像すると、これはBlackfinコアのCore Event Controler(CEC)とペリフェラルのSystem Interrupt Controler(SIC)のミスマッチが原因です。
たとえば、IMASKのあるペリフェラル(Aと呼ぶ)に対応するbitをクリアするシーケンスを考えます。安全なシーケンスは、たぶんこんな感じです。

  1. CECのIMASKをクリアし、割り込みを禁止する
  2. SIC_IMASKの値を読む
  3. 読んだ値に対して、ペリフェラルAに対応するビットをクリアする
  4. 修正値をSIC_IMASKに書き込む
  5. SSYNCを実行
  6. CECのIMASKを元の値に戻す

これは一見安全なシーケンスに見えますが、過去に試した結果では安全ではありませんでした。というのは、ステップ1でCECを割り込み禁止にしても、SICからコアに対してペリフェラルAのレベル割り込み要求が来ることを禁止できないからです。実際にステップ2の段階で割り込み要求がくると、IMASKの値にかかわらずCECのILATレジスタは割り込み要求を記録します。その後、ステップ2以降を実行すると、全ステップ終了時には次のようになっています。

そのため、全ステップ終了時にIMASKの割り込み禁止を解除すると、ペリフェラルAからの割り込みがかかるのですが、対応するSIC_IMASKはクリアされています。結果的に、プログラムから見るとスプリアス割り込みが発生していることになります。
この問題は、現在のSICアーキテクチャでは解決できません。SICによる割り込み許可動作に関しては問題ないものの、禁止に関してはアプリケーションの初期化時にしか許されません。

チケット #16714 の対策案

interrupt_dispatcher()が内部で排他制御されていないためにネストした割り込みで問題が発生します。この問題の修正方法は二つあります。

  1. 割り込みのネストを禁止する
  2. 割り込み許可タイミングをinterrupt_dispatcher()内部まで遅らせる

1が一番シンプルです。Blackfinは動作周波数が低いものでも400MHz程度あります。また、RTOSアプリでは割り込みハンドラ内部であまり重い操作をしない傾向があります。そのため、割り込みハンドラ内部を全面的に割り込み禁止にするのもありかと思います。ただし、それなりに不感期間は長くなるわけで、ためらいはあります。割り込みコードそのものは幾分の簡略化が可能になります。
2については、割り込み禁止期間を今と同程度に短くできるのがメリットです。ただし、interrupt_dipsatcher()にパラメータをひとつ増やすことになります。

rtiレジスタを復帰してグローバル割り込み許可
interrupt_dipspatcher(割り込みレベル)

としている処理を

context = cli命令でCPUロック
rtiレジスタを復帰してグローバル割り込み許可
interrupt_dipspatcher(割り込みレベル、context)

に変更するものです。interrupt_dispatcher()内部ではSIC_IMASKとSIC_ISRを読み込んだ後にCPUロックを解除します。
以上のいずれの方法も、例外ハンドラ、NMIハンドラ、管理外割り込みハンドラの内部でSIC_IMASKを変更するようなコードに対しては無力です。しかし、これについては注意書きを残した上でプログラマの責任としても問題ないかと思います。

チケット #16729 の対策

dis_int, ena_intが保護されていない問題です。これは、上に述べたBlackfinのアーキテクチャ上の制限があるため、CPUロックでは逃げることができません*2。そのため、実行コンテキストをタスク・コンテキストだけに制限しても問題は発生します。
結果的に

  1. 実装しない
  2. 割り込み起動前の初期化時以外では使わないようにメモを残す

という、消極的方法しか存在しないことになります。今考えているのは、

  • get_ims, dis_intのみ実装する。dis_int()はCPUロックで保護する。chg_ims*3とena_intは必ずビルド・エラーを起こすようにする。というものです。こうすることで、いくらかでもこの問題に対するプログラマの注意を喚起できるのではないかと思っています。

*1:割り込みを禁止した場合

*2:ena_intだけは逃げることができる

*3:chg_imsは排他制御云々の前に、初期化後呼ぶこと自体が危険