LPCマイコンのSCTimerで作るI2Cモニタ


背景


LPCマイコンに搭載されてるSCTimer。PWMのために使うには複雑すぎる、複雑なわりに他の使い道がない、などとあまり評判は良くないのですが、うまくハマる応用例があったので軽く紹介。

SCTimerについて

SCTimer/PWM

NXPセミコンダクターズから出ているARM Cortexベースのマイコンに搭載されているモジュールの1つ。Cortex-M0+クラスの用途ではPWMとかがわりと重要なんだけど、PWMにも使える反応モジュールとしてSCTimerが提供されているために「たかがPWMのために複雑すぎる……」みたいな評価が多くて比較的残念なモジュール。

具体的にできる事

GPIOを通した入力信号や自身の出力信号、あるいはカウンタの値などを入力としてイベントを定義する事ができます。GPIOを入力にする場合はLow/Highといった値に反応するイベントも定義できるし、信号の立ち上がり立ち下がりのエッジに反応させる事もできます。カウンタを入力にする場合も一致や比較結果を条件にイベントを発生させる事ができます。

内部的に定義されたイベントは、そのまま割り込みのトリガーとして使ったり、出力信号の0/1を変化させるトリガーとして使うことができます。

さらに間接的な利用方法として、内部ステートを定義してステート遷移のトリガーとする事もできます。このステートは先のイベント発生条件に含めることができるため、より複雑な信号処理も可能となります。が、低価格クラスではステートが2つしかないため、ほとんど何もできません。このため「無駄に複雑」と評価される事が多いようですが、まぁ一度どんなものか理解すれば、別に難しくはありません。

単純なPWM

任意サイクルでカウンタを回しておき最大値Nと中間の任意値Mで発生するイベントを定義し、これらのイベントをトリガーとして出力を0/1で切り替えれば、デューティ比M:N-Mの矩形出力が可能となります。

さらにPWMを利用した音声出力

Nを256とし、最大値のイベントで割り込みも発生させ、割り込みハンドラで8-bitサンプル値をMとして書き込みます。Mの値は次回のカウンタリセット時に読む値として更新できるため、PWM周期に同期した変調が可能となり、例えばクリスタルなしの12MHzで動作させた場合には、8-bit/46.875kHzの再生が可能となり、そこそこの音質が実現できます。

I2Cモニタを作ってみる

どんなもの?

I2Cバスに流れてるアクセスをUSART経由でPCでロギングするツールを考えていたのですが、これを低価格マイコンで実現しようとすると、わりと厄介ということがわかりました。I2Cをデコードしてくれるロジアナも持ってるんですが、準備するのが面倒だったりサンプリングのトリガを作ってやるのが面倒だったりするんですよね。どうせだったらバスに挿したらPCにダラダラとリアルタイムにログを吐き出し続けてくれた方が嬉しい。

ハードウェアサポート

もちろんI2C自体はマイコンが直接サポートしている事が多いのですが、それはマスターかスレーブの動作を仮定したサポートであって、バスに流れる他人宛てのデータを読み取るような機能はサポートされていません。という事でGPIOを使ってソフトウェア的に読み取ることを考える必要があります。

ソフトウェアでの実装方法

まずはナイーブにループを回して力技で読み取る方法を考えましょう。I2Cは一番低速なモードでも最大100kHzで伝送が起きるため、単純なポーリングで信号変化を4倍の400kHz程度でサンプリングする必要があります。12MHzでの動作を考えるとサンプルあたり30クロック。タイマー割り込みを使っていたら、割り込みオーバーヘッドと割り込みフラグのチェック、リセットだけで時間の大半を使いきってしまいそうです。どうしても割り込みでやるなら、割り込みでは値の取り込みだけを行い、メインループで取り込みデータの解析をするしかないでしょう。ただメインループに必要となる時間が残されているかどうか……。割り込みを諦めてループで回すなら、速度に余裕ができたとしてもタイミングを合わせるためにクロック数を数えながらアセンブリで書く事になりそうです。

次に考えるのは信号変化をトリガーとした割り込みを利用する方法。ただしI2Cの場合には、クロック信号の立ち上がりで信号を読んでいれば良い、というわけではなく、クロックが1の期間にデータが変化したタイミングもスタート、ストップとして検出する必要があります。これを全て割り込みで処理しようと考えると、最大負荷の状況では100kHzでのクロックの立ち上がり、50kHzでのデータ立ち上がり、立ち下がりで、合計200kHzの割り込みが想定されます。割り込みあたり60クロック。まだCで書くには心もとない数字です。実際、最初この方法でコードを書いてみたのですが、ときおりストップ条件の検出ミス・誤検出が発生してしまいました。

どちらの方法もアセンブリを使えばなんとか回せる範囲の問題なのですが、今回作っているのはメインの工作ではなく、その実験用のツールだったため、そこまで気力がわきません。で、最終的に落ち着いたのがSCTimerを使った方法です。

SCTimerを使った方法

I2Cをモニタする場合には2つの信号変化のAND条件で割り込みを発生させる事ができればベストです。例えばスタート条件はSDAのFalling edgeかつSCLのHigh、ストップ条件はSDAのRising edgeかつSCLのHighとして定義できます。

しかし、SCTimerでは2つの信号変化のAND条件をイベントとして設定する事はできません。少し工夫をして、片方の信号をステートに変換し、ステートと信号変化のAND条件に読み替えてみます。

  • SCLがLowだったらステート0に遷移
  • SCLがHighだったらステート1に遷移

この2種類のイベントを定義する事で、ステートをSCLとして読み替える事ができます。この際1サイクルの遅延が発生しますが、この程度の遅延であれば問題になりません。加えて以下のイベントを定義します。

  • ステート1でSDAのFalling edgeが発生したら割り込み(スタート条件)
  • ステート0でSDAのFalling edgeが発生したら割り込み(ストップ条件)
  • SCLのRising edgeが発生したら割り込み(クロック同期データ)

先の方法で取り込みが怪しかったストップ条件も、ハードウェアで条件を抑えておけば取りこぼしのリスクがなくなります。また割り込み条件の過検出もなくなり、最大割り込み負荷も100kHzでデータパタンに依存しなくなりました。これなら取り込んでメインループでUSARTにログを書いていても、バーストでアクセスがない限り耐えられそうです。万が一そのような状況でも確実に取り込みを行いたければ、ログをバイナリにするか、少しアクロバティックですが横から強制的にクロックストレッチをかけたりするしかないですかね。今回は取りこぼしを検出してエラーをログする程度でとどめてあります。

コード

SCTimer周りの記述例

// LPCで忘れがちな個別のクロック有効化処理
LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 8);

// SCTimerモジュールをリセット
LPC_SYSCON->PRESETCTRL &= ~(1 << 8);
LPC_SYSCON->PRESETCTRL |= (1 << 8);

// PINASSIGN5/6を使って事前に適当なGPIOをCTIN0/1にアサイン
// 以下はCTIN0にSCL、CTIN1にSDAをアサインした状況を仮定した定義
const uint8_t kOnState0 = 1 << 0;
const uint8_t kOnState1 = 1 << 1;
const uint32_t kOnScl = 0 << 6;
const uint32_t kOnSda = 1 << 6;
const uint32_t kOnLow = 0 << 10;
const uint32_t kOnRising = 1 << 10;
const uint32_t kOnFalling = 2 << 10;
const uint32_t kOnHigh = 3 << 10;
const uint32_t kIoMode = 2 << 12;
const uint32_t kLoadState = 1 << 14;
const uint32_t kState0 = 0 << 15;
const uint32_t kState1 = 1 << 15;

// SCLに同期してステート0/1を遷移させる
LPC_SCT->EVENT[0].STATE = kOnState1;
LPC_SCT->EVENT[0].CTRL = kIoMode | kOnLow | kOnScl | kLoadState | kState0;
LPC_SCT->EVENT[1].STATE = kOnState0;
LPC_SCT->EVENT[1].CTRL = kIoMode | kOnHigh | kOnScl | kLoadState | kState1;

// START条件
LPC_SCT->EVENT[2].STATE = kOnState1;
LPC_SCT->EVENT[2].CTRL = kIoMode | kOnFalling | kOnSda;

// STOP条件
LPC_SCT->EVENT[3].STATE = kOnState1;
LPC_SCT->EVENT[3].CTRL = kIoMode | kOnRising | kOnSda;

// クロックの立ち上がりを検出
LPC_SCT->EVENT[4].STATE = kOnState0;
LPC_SCT->EVENT[4].CTRL = kIoMode | kOnRising | kOnScl;

// カウンタを使わない場合もHOLDを解かないとイベントが発生しません
LPC_SCT->CTRL_L = 0;

// イベント2,3,4で割り込みを発生させる
LPC_SCT->EVEN = kEvent2 | kEvent3 | kEvent4;
NVIC->ISER[0] = (1 << 9);

あとは割り込みハンドラを書いてあげて、中で割り込み種別を判定して、適宜スタート・ストップ処理、データ取り込みを行います。

完成品の紹介

ソースコードと回路図一式をgithubに置いておきました。

上側の基板が今回作ったものです。ボタンはリセットとISPモードで起動するためのもの。右に刺したUSBシリアルから3.3Vをもらいつつログを吐き出します。左から別基板のI2Cバスに繋げてます。接続先はトラ技2014年2月号の付属基板。とりあえず家にあった液晶だけ繋げてあります。

シリアル経由での書き込みと取得ログはこんな感じ。

% lpc21isp -term -bin Release/I2CInspector.bin /dev/ttyUSB0 230400 12000
lpc21isp version 1.94
File Release/I2CInspector.bin:
    loaded...
    image size : 3824
Image size : 3824
Synchronizing (ESC to abort)........ OK
Read bootcode version: 4
13
Read part ID: LPC810M021FN8, 4 kiB FLASH / 1 kiB SRAM (0x00008100)
Will start programming at Sector 1 if possible, and conclude with Sector 0 to ensure that checksum is written last.
Erasing sector 0 first, to invalidate checksum. OK 
Sector 1: ...|.|.|.
Sector 2: ...|.|.|.
Sector 3: ...|.|.
Sector 0: ..|.|.|.
Download Finished... taking 1 seconds
Now launching the brand new code
Terminal started (press Escape to abort)

I2CInspector ready
3E<0038
3E<0039
3E<0014
3E<007F
3E<0056
3E<006C
3E<0038
3E<000C
3E<0001
3E<0080
3E<4054
3E<4065
3E<4073
3E<4074
3E<4031

実際に観測しているI2Cのやりとりは、観察対象基板に乗っているマイコンがLCDに対して初期化、"Test1"の文字書き込みを続けて行っているところです。初期化部分はデバイスを待つための遅延ループがあったりして負荷的には軽いのですが、文字列の部分は連続書き込みをしています。ここで特に取りこぼしが発生していないので、今回の試みはひとまず成功。


最後のやりとりをロジアナで見たのがこの1枚。直前の4074書き込み後のStop conditionは十分に短く、続けて3E<4031の書き込みが発生していたのが確認できます。

最後に

と、たまたま必要としていた要件にドンピシャな仕様だったわけですが。どうですかね、個人的には汎用PWMよりは応用先が決まっていてもΔΣ変調とか載せといてくれたほうが嬉しいのですが。あれ、マイコンでソフト処理するのは無理だけどハードなら回路は簡単だし、同じ周波数でもPWMよりずっと特性の良い出力が得られますからね。