SCC+互換チップについて
LPC810をPSG/SCC+互換チップとして動作させるファームウェアを作り、過去にいくつかエントリを投稿しました。
最近になり、TwitterでSPIのサポートもして欲しいという要望を頂き、なるほどと思いサポートするコードを追加したので、その際にハマった点のまとめと、使い方の紹介などを書き留めておこうと思います。
ちなみに、こんな要望。
楽しみにしてます! ついでですが、SPI対応になったりすると俺がもっと喜びます!
— 魔弩災炎(キュアホイコーロー) (@madscient) 2017年8月16日
利点について聞いてみたら、確かに簡単にサポートできれば可能性が広がるかな、と思った次第。
なるほど。SPIだと、同種複数盛り(たとえばSCC4個とか)がやりやすいかなと思ったのでした。まあ間にマイコンもう1個かませばいいんですけど。
— 魔弩災炎(キュアホイコーロー) (@madscient) 2017年8月16日
I2C Slave マイコンから制御したいような場合にSPIだと選択肢になって嬉しかったり
— フリブル@ホロウイルス感染中 (@tadfmac) 2017年8月20日
LPC810でSPIサポートする際のハマりどころ
SWMにクロックを入れるべし
LPC810でハマりがちなのが、必要なブロックにクロックを入れ忘れるミス。忘れてても黙ってスルーされるだけなので、結構気づきにくい。で、基本的にはI2Cの代わりにSPI0に対してクロック入れたり、ペリフェラルにリセット入れたりするわけですが、うっかりハマったのがSWM。
実はそもそもI2Cでもピンアサインは必要なので、SWMにはクロックを入れておくのが順当だったのですが……入れずに動いたので、敢えて消してしまったんですね。これを忘れてました。SWMのクロック供給なしで、実際どこまで動いてどこから動かないのか不明なのですが、少なくともSPIに関してはSWMにクロックを入れるまでは入力が全然取れていないようでした。
1ピンの扱い
I2CではSCLが1ピン、SDAが8ピンという配置だったので、SPIでもなるべく似た配置を取りたいと思いました。よって、SCKを1ピン、MOSIを8ピン、そしてSSELを唯一空いていた5ピンに配置します。MISOに関しては外に出さない事にしました。どうしても必要という声があれば、SWDIOあたりを潰す事になるでしょうか。
ここで1ピンに関して問題がありました。1ピンは元々リセットが割り当てられており、起動直後にリセットを無効化してペリフェラル用のピンとして再利用しています。マニュアル的には50ns程度の↓↑パルスでリセットがかかることになっており、レベルトリガにはなっていないように思われるのですが、PON時に限っては、lowを保持していた場合、一旦highになるまでリセットがかかったままになるようです。マニュアルのどこかに書いてありそうな案件ではありますが、I2CのSCLがたまたまidle時にオープンドレインでプルアップという仕様だったため、意識していませんでした。
SPIの場合、SCKはモード0/1においてはidle時にlowです。よって、PON時の問題を回避するためにはモード2/3のみが選択肢に入ってくる事になります。
(追記 2017/9/3)SPIマスタ側の初期設定にもよるのかもしれませんが、手元ではモード0でもうまくデバイスのリセットが解除されるようになりましたので、素直にモード0を使うように変更しました。もし反応がないようでしたら、マスタ側起動時にSCKのピンをしばらくhighにしておく、あるいは↓↑してからhighを保持する事で動作するかと思われます。PLL安定直後にRESETピンを殺してるので、1msecも待てば十分すぎるかと思います。この際、同時に5ピンもhighにしておくと確実です。
5ピンの扱い
今まで空きだった5ピンについても扱い要注意です。なにせISPモードのエントリになっているのですから……。電源投入時に5ピンがlowになっているとISP書き込み待機モードになってしまいます。マスター側は電源投入と同時にSSELをhighにするなり、基板上でpull upしておくなりする必要があります。
SPIモジュールの微妙な挙動
LPC81x系のSPIモジュールに関しては、既にChaN先生が色々と実験をされており、master時にはmode 0/2が癖のある挙動をするので要注意、な扱いとなっています。適切に処置すれば正しく動作させられるようでもあるのですが、あまりSPIに慣れていない自分としてはmode 0/2は避けて通りたい気分です。
(追記 2017/9/3)slaveで受けるだけなら別段変な癖もなく動いているようです。
そうなると、必然的に利用可能なモードは2/3と1/3のANDという事になり、モード3を選ぶのが妥当、という事になります。
そうは言いつつも、こっちはslaveなので、本当に安定して動くモードがどれなのか疑心暗鬼のまま作業しなければなりません。ロジアナ必須です。
ちなみに、最初はSSELはlow固定でいいやと思って実験していたのですが、読み込む際のビット配置が少しずつずれていくので駄目でした。16-bit送る前後に↓↑してあげましょう。
Release buildでbreakpointが止まらない
なんか、そういう仕様だった気もするし、そうでなかった気もするし。久しぶりに開発環境触ったのではっきりと覚えていないのですが。以前使っていたLPCXpressoは最近のUbuntuでは不安定だったので、最新のMCUXpressoを使う事にしました。が、指定したはずのRXRDYの割り込みハンドラのbreakpointで全然止まってくれず、しばらく時間を無駄にしてしまいました。Debug buildで作業するのが正解です。
SPIからの使い方
レジスタアクセスに関して
転送長を16-bitとし、上位8-bitが内部レジスタアドレス、下位8-bitが書き込みデータとしました。また、I2Cのようなslave addressの概念がないため、マップする内部レジスタを切り替えるページの概念を導入します。内部レジスタ0xFFを特殊なアドレスとし、ここの書いた値により0x00-0xFEにマップするレジスタ(ページ)を切り替えます。ページの番号はI2C slave addressと同じにしてあるので、0x50がPSG、0x51がSCC+となります。
Arduino/STM32duino用のサンプル
とりあえず自分はSTM32duinoを使って試したのですが、Arduinoでもピンさえ間違えなければ、ほぼそのまま動くかと思います。
#include <SPI.h>
#define SSEL PA4
#define SCK PA5
void write(uint8_t addr, uint8_t data) {
static uint8_t bytes[2];
bytes[0] = addr;
bytes[1] = data;
digitalWrite(SSEL, LOW);
// transfer16はSTM32duinoではうまく動かず8-bit転送になります。
// setDataSize()で明示的に指定する必要があるようですが、ここではwriteを使いました。
SPI.write(bytes, 2);
digitalWrite(SSEL, HIGH);
}
// 今回は呼んでないけど、こんな形で書き込み先のレジスタを切り替えられます。
// 起動時にはPSGレジスタがマップされているため、SCCを使う際には必須です。
void activate_psg_registers() { write(0xff, 0x50); }
void activate_scc_registers() { write(0xff, 0x51); }
void setup() {
// SSELはISPモード防止のためhighにしましょう。
pinMode(SSEL, OUTPUT);
digitalWrite(SSEL, HIGH);
// 念のためリセット解除相当の信号を流してして少し待つ。
pinMode(SCK, OUTPUT);
digitalWrite(SCK, HIGH);
digitalWrite(SCK, LOW);
digitalWrite(SCK, HIGH);
delay(1);
// ここまでのコードはなくても動くかも。デバイスの初期設定と回路次第。
// あまり良い例ではないかもしれませんが、とりあえずbeginで開けっ放しに。
// MSBFIRSTとSPI_MODE3の設定は必須、速度的には10MHzくらいまでいけると思います。
SPI.beginTransaction(SSEL, SPISettings(10 * 1000 * 1000, MSBFIRST, SPI_MODE3));
// 音をプーっと鳴らすだけ
write(0x00, 0xfd);
write(0x01, 0x01);
write(0x07, 0xfe);
write(0x08, 0x0f);
}
void loop() {}