PIC32でSPIの送信

SPI communication by PIC32
 
SPIは通信相手(スレーブ)の選択やフレーム同期を行うSS/SSIと、ビットを知らせるSCK、データ入出力・SDO/SDIの4線から成り立つ通信方法で、PIC32MXの場合、最大で周辺クロックの1/2の速度(40MHzクロックなら20MHz)を出すことができます。

SCKのピンは固定(B14/B15)ですが、それ以外は複数のピンの中から選ぶことができます。

SDI1A1(0b00)B5(0b01)B1(0b10)B11(0b11)B8(0b100)
SDO1(0b11)A1B5B1B11
SS1A0(0b00)B3(0b01)B4(0b10)B15(0b11)B7(0b100)
SDI2A2(0b00)B6(0b01)A4(0b10)B13(0b11)B2(0b100)
SDO2(0b100)A1B5B1B11
SS2A3(0b00)B14(0b01)B0(0b10)B10(0b11)B9(0b100)
(26ピンモデルの場合。括弧内はレジスタに登録する数値)

SPIの方式は、同期信号が立ち上がったとき/立ち下がったとき、マスターかスレーブか、同期信号でビット列のグループを判断するかどうかなどで、信号の内容が少しずつ変わってくるため、SPIxCONレジスタで細かく指定する必要があります。

SRXISEL拡張バッファ使用時のSPI受信割り込みのタイミング
0-バッファの中身がすべて吐き出された
1-バッファが空でなくなった
2-バッファの半分以上が埋まった
3-バッファがすべて埋まった
STXISEL拡張バッファ使用時のSPI送信割り込みタイミング
0-送信が完了した
1-バッファが空になった
2-バッファが半分以上からになった
3-バッファに空きができた
DISSDISDIピンの使用(1-使用しない)
MSTENマスターモード(1-マスター:0-スレーブ)
CKP1でクロックがない状態をHighとみなす
SSEN1でスレーブ用にSSxピンを使用する
CKEデータの取得タイミング(0-クロックが開いている[Active]ときに取得:1-閉じている[Idle]ときに取得)
SMPマスターでのデータ受信タイミング(0-クロックの中央:1-クロックの終わり)
MODE1616bit単位で転送(0で8bit単位)
MODE3232bit単位で転送(0ならMODE16の値が適用される)
DISSDOSDOピンの使用(1-使用しない)
SIDLアイドル中の停止(1-停止する)
ONSPIの実行(1-実行する)
ENHBUF拡張バッファ(FIFOバッファ)の使用(1-使用する)
SPIFE同期エッジの位置(0-立ち下がり:1-立ち上がり)
MCLKSELマスタクロックを使用(0-周辺クロックを使用)
FRMCNTフレーム同期信号のタイミング(0-1データごと:1-2:2-4:3-8:4-16:5-32)
FRMSYPWフレーム同期信号の長さ(0-1クロック:1-MODE16/32で指定したビット数)
MSSENスレーブ選択の可否(1-スレーブを選択できるようにする)
FRMPOLフレーム同期信号の極性(0-Lowで開く:1-Highで開く)
FRMSYNCフレーム同期信号の向き(0-出力/マスター:1-入力/スレーブ)
FRMENフレームモードで使用(1-使用)

信号のタイミングは言葉では伝わりにくいので、データシートの内容も引用しておきます。
なおCKP/CKEの組み合わせとArduinoにおけるSPIモードの関係性は以下のようになります。
ModeCKPCKE
Mode001
Mode100
Mode211
Mode310
SPIの周波数は「PBCLK/(2*BRG+1)」で計算します。例えばPBCLKが40MHzの場合、BRG=0なら20MHz、BRG=15なら1.25MHzになります。

それではサンプルを作ってみましょう。今回はSPI通信でGPIOの制御ができるマイクロチップのI/Oエクステンダー「MCP23S17(日本語データシート/PDF)」を使用します。ピン数の少ない「MCP23S08」も基本的な操作方法は同じますが、MCP23S17の方がライブラリーなどの情報が比較的豊富です。

ArduinoではSIはMOSI(No.11)、SOはMISO(No.12)に接続しますが、PICの場合はSIはSDO、SOはSDIにつなぐ点に注意しましょう。またMCP23S17が受信できる信号速度は最大10MHzなので、送信時のクロックも適時落としておきます。I/Oの切り替えは、SSをオフにした間に、3バイトのデータから成る命令をSPI信号として送信することで実行されます。
main.c
#include <p32xxxx.h>
#include <sys/attribs.h>

// 外部の8MHz水晶発振器から40MHzのクロックを生成するための設定
#pragma config FNOSC = PRIPLL // 発信源 = 主外部発振器
#pragma config POSCMOD = XT // 主発振の方法 = 外部高精度発振器を使う
#pragma config FPLLIDIV = DIV_2 // PLL入力分数 = x1/2
#pragma config FPLLMUL = MUL_20 // PLL逓倍比 = x20
#pragma config FPLLODIV = DIV_2 // PLL出力分数 = x1/2
#pragma config FPBDIV = DIV_2 //周辺モジュールクロック倍数 : x1/2

void send(int addr, int data)
{
    LATBbits.LATB0 = 0;
    int i, n;
    for(i = 0; i < 3; i++){
        switch(i){
        case 0: n = 0x40; break;
        case 1: n = addr; break;
        case 2: n = data; break;
        }
        SPI2BUF = n;
        while(SPI2STATbits.SPIBUSY) ; // 処理が完了するのを待つ
    }
    LATBbits.LATB0 = 1;
}

int main(void)
{
    int i, j = 0;
    ANSELB = 0x00;          // ポートBをすべてデジタルモードにする  
    TRISB = 0;              // ポートBをすべて出力にする
    
    LATBbits.LATB0 = 1;     // B0 = on   
    
    SPI2CONbits.ON = 0;     // 設定のためSPI1を無効に
    
    SPI2BRG = 15;           // SPIの周波数を2MHzに
    SPI2CONbits.MSTEN = 1;  // マスターモード
    SPI2CONbits.CKP = 0;    // ArduinoにおけるMode0
    SPI2CONbits.CKE = 1;    // ArduinoにおけるMode0
    
    SDI2Rbits.SDI2R = 0b0100; // B2にSDI2を割り当てる
    RPB1Rbits.RPB1R = 0b0100; // B1にSDO2を割り当てる
    SS2Rbits.SS2R = 0b0010;   // B0にSS2を割り当てる
    
    j = SPI2BUF;            // ダミーに出力してバッファをクリア
    
    SPI2CONbits.ON = 1;     // SPI2を稼働
    
    //  MCP23S17の設定
    // 1バイト目はDevice Opecode。0x40で書き込み命令、0x41で読み込み命令
    // 2バイト目は制御レジスタ
    // 3バイト目はパラメータ
    send(0x0A, 0x20);       // IOCON[0x0A]、シーケンシャルモードを無効[SEQOP = 1]
    send(0x00, 0x00);       // IODIRA[0x00]、ポートAのすべてのピンを出力モードに
    
    while(1){
        send(0x12, j);      // GPIOA[0x12]、すべてのピンのオン(1)・オフ(0)を切り替える
        j = ~j;             // 0x00<->0xFFに切り替え
        for(i = 0; i < 1000000; i++) ;
    }
}
SPI通信が成功したのを確認したら、LED(と抵抗)の数を増やし、jの値をビットシフトさせてキラキラさせてみましょう。


(おまけ)SPI信号のテストのために作ったArduinoスケッチ
spiduno.ino
#include<SPI.h>

#define SS 10

#define MCP_WRITE 0x40
#define MCP_READ 0x41

void sendBytes(int address, int data)
{
  digitalWrite(SS, LOW);
  SPI.transfer(MCP_WRITE);
  SPI.transfer(address);
  SPI.transfer(data);
  digitalWrite(SS, HIGH);
}

void setup()
{
  pinMode(SS, OUTPUT);
  digitalWrite(SS, HIGH);

  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2);

  sendBytes(0x0A, 0x20); // I/Oの初期設定[SEQOP:1(バイトモード)]
  sendBytes(0x00, 0x00); // ポートAをすべて出力にする
}

int p=0;
void loop() {
  sendBytes(0x12, 1<<p);
  p = (p == 0) ? 7 : 0;
  delay(200);
}
2018/09/28