SPI ってお好きですか?

(更新

Do you like SPI (Serial Peripheral Interface)?  I needed to troubleshoot a tough SPI problem between STM32 MCU and “an” Atmel peripheral today.

皆様は SPI(シリアル・ペリフェラル・インタフェース)ってお好きですか?  私はというと、選択肢があるなら UART のほうを選んでしまいます。もちろん、SPI のほうが(比較的)高速通信を実現しやすいですし、UART とは違って、上流のフレーミングプロトコル無しにメッセージ境界を明確にできる、というメリットはあるのですが。

ここ 3日ほど、SPI で非常にタフ(難解)な問題に取り組んでいたのですが、ようやく原因が分かったので、めでたく(?)御紹介させて頂きます。非常にレアな問題だと思うのですが、同じような問題に遭遇する方もあるかも知れません。

何が問題か?

「問題」というのは、STMicroelectronics の STM32 マイコンと、旧 Atmel(以下「旧」を略)の某ペリフェラルを SPI で接続したときに、ときどき受信データが誤る、というものです。それも、ノイズや波形から生じるエラーではなく、決定論的に、同じところでは常に必ずデータが誤る、という現象です。

そもそも、このペリフェラルの SPI 通信プロトコルは明確でなく、専用のドライバソフトを使って利用することになっているという厄介な代物です。(何か問題が出たら、ドライバを逆エンジリニアリングしないといけない。) さらに、一般の他社 MCU に繋ぐことはあまり想定しておらず、同社(つまり Atmel)の MCU のサンプルコードを読みながら(可能ならば動かしながら)対応しないといけない、という点も厄介です。

まず、結論

結論から言いますと、STM32 の SPI HAL の中で、SPI ペリフェラルの de-activation 処理が早すぎるため、かつ、Atmel ペリフェラルの SPI COPI (Controller Output Peripheral Input) 受信動作がやや特殊なため、後続の CIPO (Controller Input Peripheral Output) データが壊れてしまう、という現象であることが分かりました。以下に、Atmel 側のデータシートからタイミング図を引用します。

ここで、t_HDSSN(SSN、つまりペリフェラルセレクト信号のホールド時間)が最低 5.5 ns 必要とされていますが(本当に短い時間なのですが)、おそらく(あまりに短すぎてオシロでも確認できない)STM32 側の SSN の deassert が早すぎるために、Atmel ペリフェラル側が SPI シーケンスを正しく終了できていないものと思われます。

以下に、不正なシーケンス(上)と正しいシーケンス(下)を示します。黄色がクロック、水色が SSN です。(オシロの画面の中で、上半分が実際に取り込んだ波形、下半分は拡大図です。)

違いがお分かりになりますでしょうか!?。まるで往年のクイズ、「七つの違い」ですね。 🙂

(ペリフェラルから見て)この CIPO 送信処理は正しく動作しているように見えるのですが、その後続の(マイコンから見て)CIPO 受信において、先頭ビットが誤って受信されてしまうのです。おそらく、前者のオシロ画面におけるシーケンスでは SSN が早く立ち上がりすぎているので、Atmel ペリフェラル側が SPI のシーケンス処理が正しく完了できていないのが原因でしょう。

さて、「後続の SPI 通信」を示します。本来は CIPO の先頭が 0x01 のはずなのですが、0x81 になってしまうのです。

SPI 余談

御興味ある方は、もう少しお付き合いください。TL;DR 🙂

SPI というのは単一の明確な規格ではなく、CPOL や CPHA の極性などから少なくとも 4つの亜種があり、さらにデータ長が 8ビットの倍数でないもの、同時に read/write するものやそうでないもの(ペリフェラルセレクト信号をどこで deassert するか、など)、ひどいペリフェラルでは、COPI と CIPO で CPHA が逆になっているというものまであります。

もう一つの問題は、上記の煩雑さに反して、メーカーが提供している SPI HAL(ドライバ)が、特定の使い方しか想定していないことが多い、ということがあります。実際、STMicroelectronics STM32 の HAL は、特殊な使い方をしづらい仕様です。実際、SPI 周りで HAL だけでコードを書けるのは、希なケースかもしれません。

解決に至るまで

話を戻します。今回、この Atmel ペリフェラルデバイスのドライバを STM32 マイコン用に書き換えたのは良いのですが、動かしてみると正しく動作しないことが分かりました。そもそも、ドキュメントがないので正しく通信できているかどうかの判断が難しいのですが、ドライバのある部分でメッセージフォーマットのエラーが起きているのを見つけ、これはどうやら難物だぞ、と思い始めました。

幸い、手元に Atmel 社純正の SAMD21 マイコンボードがあったので、また上記ペリフェラルも Atmel の評価ボードだったので、遠回りにはなりますが、まずは Atmel のサンプルプログラムを動かしてみることにしました。たまたま手元にあったから良かったのですが、なかったら問題解決はますます遅れていたことでしょう。

さらに加えて、SPI 通信コードに printf() 文を入れ、送信データと受信データを全てダンプしながら動かしてみました。SPI インターフェイスにはオシロも繋ぎ、全ての信号線をモニタしました。すると、送信データは正しいものの、SAMD21 での動作と STM32 での動作を比較すると、マイコンによる受信データが異なることがある、という現象を見つけました。通常、デバイスは決定論的に動作しますので(RTC や乱数発生器などを除く)、送信データが同じなのに受信データが異なる、ということはあり得ません。

オシロとにらめっこを繰り返しますが、なかなか分かりません!  最初は(私の書いた)STM32 の SPI 受信コードが間違っているのかと思ったのですが、オシロで見ても実際に 0x81 になっています(上記オシロ波形参照)。

光明が差す!

しばらく原因が分からず窮していたのですが、たまたま、コンパイラの最適化オプションを -Og から -O0 に変えてみたところ、なんと正しく動くようになりました。

最初は、ははーん、私のコードにバグがあったかな?  と思ったのですが、ファイルを一つ一つチェックしていくと、私のコードではなく、STM32 の HAL ソースのコンパイルオプションを変えたときに現象が異なるようになる、ということが分かりました。STMicroelectronics のバグなのでしょうか!?

しかし結論としては、既に上で述べたように、STMicroelectronics と Atmel の SPI 通信仕様の差が、原因であるようです。つまり、どちらか一方の問題ではなさそうです。STM32 の HAL としては、SSN の deassert が早すぎるという問題がありそうですが、そもそも、SPI の CIPO 受信が完了したら、マイコン側としては SPI ペリフェラルをリセットしてしまって、何の問題があるのでしょうか。一方で、Atmel 側としては SSN の deassert が早すぎると SPI 通信シーケンスが正しく終了できず、後続の通信に影響を与えてしまう、という SPI 回路上の問題がありそうです。

ワークアラウンド

といことで、ワークアラウンドを盛り込みました。ここが本当の「結論」でしょうか。HAL に HAL_SPI_TransmitReceive() という関数があります。後半のほうで、

*((uint8_t *)hspi->pRxBuffPtr) = *((__IO uint8_t *)&hspi->Instance->RXDR);

という受信処理の後、

/* Call standard close procedure with error check */
SPI_CloseTransfer(hspi);

という後処理(SPI ペリフェラルのクローズ処理)がありますが、これが早すぎると今回のような現象に繋がります。この間に busy wait を入れてやれば OK です。本当はもっと洗練された方法(フラグを見ながら待つ)があれば良いのですが、ちょっと分かりませんでした。今回は SPI を 400 kHz 程度で使っているのですが(いずれクロックを上げる予定)、1クロック周期に相当する 2.5 us 程度のウェイトを入れてみました。因果律(?)から考えると、CIPO のデータをマイコンが読み取れた事後ですので、SCLK(SPI クロック)が最後に(つまり 8ビット目に)立ち上がってから 2.5 / 2 [us](マイクロ秒です) + 5.5 [ns](ナノ秒です)を待てば、SSN を立ち上げて問題ないと思われますので、2.5 us で十分なはずです。言い換えると、SPI_CloseTransfer() を呼ぶまでに 2.5 us 待てば、おそらく Atmel ペリフェラル側の SPI 受信シーケンス処理は完了していることでしょう。

ちなみに余談ですが、今回評価している STM32 マイコンは Cortex-M7 を搭載し 400MHz で動作する最新のものです。このような高速な MCU であることも、今回の問題に繋がった遠因かも知れません。(実際、最適化オプションを外すと正しく動作するようになる。)

総括

サラリーマン時代を含めて、今までいろんな問題に遭遇してきましたが、今回の問題はかなり難解な部類に属するものでした。しかし端的に言えば、電子回路は物理的現象に基づくものですし、ソフトウェアは論理的に動作するものです。ですから、オシロスコープや print デバッグを丁寧に使っていけば、いつかは光明が見えるはず、という自信を(少しだけ?)得ることができました。

もう一つ。SPI はやっぱり面倒くさい!  そう思うのは私だけでしょうか? 🙂

今日はここまで。