APDahlen Applications Engineer
この技術概要では、シーケンシャルロック(seqLock)技術を用いて、割り込みサービスルーチン(ISR)からmain()へミリ秒カウント値を転送する方法を示します。この技術により、マイクロコントローラのグローバル割り込みを無効化することなく、安全にデータを転送できます。この技術は全てのマイクロコントローラに広く適用可能であり、図1に示す PIC16F13145 を用いて説明します。
この記事は、前回のミリ秒コールバックに関する記事の続編です。MPLAB Code Configurator(MCC)によって生成されたコールバックがこのアプリケーションでどのように使用されるかをより深く理解するには、前提条件となる記事を確認してください。
図1: ブレッドボードに取り付けられた PIC16F13145 Curiosity Nanoと押ボタンの写真
技術的なヒント: アトミックという用語は、seqLockを理解するための基本的な前提条件です。アトミックとは、分割できない最小のビット幅、つまり中断されずに1つのステップで完了できる最小単位と定義されます。対象のPIC16では8ビット、ARMなどのマイクロコントローラでは32ビットです。
変数がマシンのアトミック幅を超えている場合、ISRとmain()間でデータを転送する際に問題が発生します。この問題は、8ビットのPICが32ビットのミリ秒カウントを転送する場合に発生します。この問題を軽減するには、グローバル割り込みを無効にする か、このノートで説明されているseqLockのようなデュアルロック機構を使用する方法があります。
シーケンシャルロックとは何でしょうか?
シーケンシャルロック(seqLock)は、2 つの独立したコードセクション間でデータの非アトミック転送を可能にするメカニズムです。このメカニズムを理解するために、まずISRの動作を確認します。
なぜISRを使うのでしょうか?
このPIC16アプリケーションでは、ハードウェアタイマを介して1ミリ秒ごとに1回トリガされる コールバック関数を確立しました。
-
TMR0は内部32MHzクロックから直接駆動されます。
-
TMR0はHFINTOSCの精度特性を引き継ぎます。必要であれば、外部水晶発振器に切り替えることで精度を改善できます。
-
コールバックはmain()とは独立して実行されます。言い換えれば、main()内で何が起こってもコールバックのタイミングには影響しません。.
-
main()から ISRへの切り替えには割り込み遅延があります。しかし、ISR内で継続されるミリ秒カウントの全体的なタイミングに比べれば、これは無視できます。
なぜミリ秒のカウントをmain()に転送するの でしょうか?
まず、フォアグラウンド処理とバックグラウンド処理という用語の定義から始めましょう。
-
フォアグラウンド: ISRは優先順位の高いフォアグラウンドで動作します。人間で言えば、ISRは反射神経系のようなものです。物事は最小限の計算で非常に速く起こります。すべてのクロックサイクルが重要です。
-
バックグラウンド: main()のコードはバックグラウンドで動作します。これは高次の脳内処理と考えてください。タイミングは重要ですが、物事はもっと落ち着いています。バッテリ電力を少しでも節約しようとしない限り、数百クロックサイクルを見落としても問題になりません。
スーパーループのタイミングは可変ですが、ISRは可変ではありません。
mainのwhile(1)スーパーループを通過する時間は非常に変化しやすいことを思い出してください。コードの種類や複雑さによって、数マイクロ秒から数秒の幅があります。このように非常に変化しやすいシステムで時間を追跡するのは、ほとんど不可能でしょう。
しかし、ISRのタイミングは高度に予測可能です。ISRはミリ秒単位のカウントを簡単かつ正確に保つことができます。
両方の長所を活用
ISRとスーパーループの特性を利用して、ISRで時間を保ち、必要なときにmain()に転送します。main()の正確な値は1ミリ秒までずれる可能性があることを理解した上で、これを行います。例えば、main()内での1クロックサイクルの違いが、ミリ秒カウントに古い値を使用するか新しい値を使用するかの違いとなる可能性があります。
なぜISRですべてを実行しないのでしょうか?
これは可能ですが、かなりのプログラミングスキルが必要です。すべてのコードサイクルがミリ秒単位のタイムスロット内で完了する場合にのみ機能します。この協調的な環境では、少しでもミスをすれば時間が無駄になってしまいます。私見では、頭を悩ませる価値はありません。ISRをタイムキーパーとして使用し、一括処理はmain()内で実行してください。
seqLockはどのように動作するの でしょうか?
seqLockは、小さなISRコールバックとmain()から呼び出されるリーダ関数の2つの部分から構成されています。
seqLockのISR部分
ミリ秒カウンタは、リスト1に示すようにMy_1ms_Callback内でわずか3行のコードで実装されています。揮発性のseq変数は、ミリ秒アキュムレータがインクリメントされる前と後の両方でインクリメントされます。この二段階ロックが、非アトミックデータの適切な転送に不可欠であることは、後ほどご説明いたします。
最初のステートメントは奇数と指定されており、加算演算後のseqは奇数となります。seq++ステートメントにより、ISRを終了する際にseq変数は偶数となります。
static volatile uint8_t seq = 0u;
static volatile uint32_t msAccum = 0u;
static void My_1ms_Callback(void){
seq++; // Begin write (odd)
msAccum++; // Increment the 1 ms accumulator
seq++; // End write (even)
}
リスト1: seqLockのISRはわずか3行のコードで構成されています。
seqLockの関数部分
リスト2は、seqLockの読み取り、チェック、リトライの部分を示しています。重要なことは、seqの前後の値を取得することです。その後、s1とs2の値を比較し、s1がs2と等しい場合にのみ処理を続行します。
ISRはmain()関数に対して任意のタイミングで発生する可能性がある点に注意してください。s1とs2のアトミック転送により、v = msAccumの転送処理中(PIC16では4回の操作)にISRの更新が発生したかどうかを検出できます。衝突が発生した場合はリトライします。
main()関数内からのseqLockの使用
完全なPIC16デモプログラムはリスト3のように示されています。この結果は、よく知られたArduinoスタイルのmillis関数を以下のように呼び出すことで実現されています。
uint32_t now = millis( );
なお、I/Oピンの設定はMPLAB Code Configurator(MCC)を使用して行います。また、My_1ms_Callback(void)関数は、main()関数内でTMR0_OverflowCallbackRegister(My_1ms_Callback);というステートメントを用いて登録されます。
uint32_t millis(void){
uint8_t s1, s2;
uint32_t v;
do { // read, copy, retry
s1 = seq;
v = msAccum;
s2 = seq;
} while ((s1 != s2) || (s1 & 1u)); // Retry if changed.
return v;
}
リスト2: millis()関数は、ISRから非アトミックデータを安全に転送するためのトライ/リトライ方式を特徴としています。
技術的なヒント: ISRとmain()は時間的に同期していません。考慮すべき2つの極端なケースがあります。
短いスーパーループ:メイン関数()が、ISRがカウンタを読み取る前に、millis()を何千回も呼び出す状態
長いスーパーループ:多くのISRコールバックが、main()から読み取られる前にカウンタを進める状態
どちらの場合でも、ISRはミリ秒カウンタを保持し、millis()が呼び出されたときに最新の値がmain() で使用できるようになります。
#include "mcc_generated_files/system/system.h"
#define HEARTBEAT_MS (500u)
static volatile uint16_t msTicks = 0u;
static volatile uint8_t seq = 0u;
static volatile uint32_t msAccum = 0u;
/**
* @brief 1 ms timer overflow callback (registered with MCC).
* @details seqLock pattern: bump @c seq before and after updating @c msAccum
* so readers can detect mid-update without disabling interrupts.
* Also toggles LATB5 every @c HEARTBEAT_MS as a heartbeat.
* @note Register with TMR0_OverflowCallbackRegister(My_1ms_Callback).
* Assumes LATB5 has been configured as an output by MCC.
*/
static void My_1ms_Callback(void){
seq++; // Begin write (odd)
msAccum++; // Increment the 1 ms accumulator
seq++; // End write (even)
if (++msTicks >= HEARTBEAT_MS) { // Heartbeat LED every 500 ms
msTicks = 0u;
LATBbits.LATB5 ^= 1;
}
}
/**
* @brief Atomic millisecond read via seqLock.
* @details Read @c seq, then @c msAccum, then @c seq again.
* Retry if the version changed (s1 != s2) or if the start version
* was odd (writer in progress on architectures that allow nesting).
* On PIC16, nesting doesn’t occur, but we keep the odd check for portability.
* @return Monotonic millisecond count (wraps after ~49.7 days).
*/
uint32_t millis(void){
uint8_t s1, s2;
uint32_t v;
do { // read, copy, retry
s1 = seq;
v = msAccum;
s2 = seq;
} while ((s1 != s2) || (s1 & 1u)); // Retry if changed. Keep (s1 & 1u) as a portability reminder if this code is ever transferred to a uC that allows ISR nesting.
return v;
}
int main(void){
SYSTEM_Initialize();
TMR0_OverflowCallbackRegister(My_1ms_Callback);
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
uint32_t now = 0;
uint32_t last = 0;
LATBbits.LATB4 = 1; // Prime the pump
while (1){
now = millis( );
if (last != now){
last = now;
LATBbits.LATB4 ^= 1;
}
// Add code here
}
}
// Doxygen comments generated using GPT-5o.
リスト3: seqLock技術のPIC16デモプログラム
seqLockプログラムのデモンストレーション
リスト3のコードは、PIC16F13145 から以下の2つの物理的な信号を出力します。
-
ISR生成信号: ピンB5に1Hzの出力
-
main()生成信号: B4ピンに500Hzの矩形波出力
これらの信号間の関係は、Digilent Analog Discoveryを使用してキャプチャした図2に示されています。時間0msで安定した一致が見られ、millis()がエラーなしで転送されたことがわかります。これは決定的な証拠ではありませんが、信号の関係は観察されたテストの間、安定したままでした。
図2: ISRとmain()の波形を比較したPIC16 seqLockの検証
seqLock技術の効率性
seqLock ISRの効率性は、リスト4に示す逆アセンブリコードによって最もよく示されています。seqLockのISR部分を実装するのに必要なアセンブリコードは合計11行だけです。これは整然とした高速なISRとして非常に望ましいことです。
! seq++; // Begin write (odd)
0x9F: MOVLW 0x1
0xA0: MOVLB 0x0
0xA1: ADDWF seq, F
! msAccum++; // Increment the 1 ms accumulator
0xA2: MOVLW 0x1
0xA3: ADDWF msAccum, F
0xA4: MOVLW 0x0
0xA5: ADDWFC 0x29, F
0xA6: ADDWFC 0x2A, F
0xA7: ADDWFC 0x2B, F
! seq++; // End write (even)
0xA8: MOVLW 0x1
0xA9: ADDWF seq, F
リスト4: seqLock ISR の逆アセンブリ コード
おわりに
seqLock技術により、グローバル割り込みを無効にすることなく、非アトミック変数をISR からmain()に転送することができます。その結果、ISRの処理は小さくなります。
この時点で、seqLockとデュアルロックまたは GIEマスク転送を比較する必要があります。どちらが速いのか、GIEビットを無効にするとコード内の他のISRにどのような影響があるのか。それはまた別の機会にしましょう。
ご健闘をお祈りします。
APDahlen
著者による関連記事
この記事が気に入った場合は、以下の関連記事も役立つかもしれません。
- Getting Started with Microchip’s Configurable Logic Cells (CLC): Turn Your PIC16 into a 555 Timer
- Microchipの PIC16 の構成可能ロジックブロックを使用したステッピングモータドライブ
- https://forum.digikey.com/t/dec-3-5625-upload/63637
著者について
Aaron Dahlen氏、LCDR USCG(退役)は、DigiKeyでアプリケーションエンジニアを務めています。彼は、技術者およびエンジニアとしての27年間の軍役を通じて構築されたユニークなエレクトロニクスおよびオートメーションのベースを持っており、これは12年間教壇に立ったことによってさらに強化されました(経験と知識の融合)。ミネソタ州立大学Mankato校でMSEEの学位を取得したDahlen氏は、ABET認定EEプログラムで教鞭をとり、EETプログラムのプログラムコーディネーターを務め、軍の電子技術者にコンポーネントレベルの修理を教えてきました。
Dahlen氏は、ミネソタ州北部の故郷に戻り、コンデンサ探しから始まった数十年にわたる旅を終えました。 彼の物語はこちらからお読みください。

