PIC16F19155を使ってみる(その7)

前回は、RTCCの内容を読み取ってEUSARTに出力することをやりました。
alasixosaka.hatenablog.com

今回は、SPIのテストです。

SPIの泥沼にはまる。

ところが、2日間ほど泥沼にはまって抜け出せずに苦戦してしまいました。
結論から書くと、PPSの設定ミスと、SPIのモードの理解不足が原因だったようです。
PPSの設定は、EUSARTのテストの時に自分で書いておきながら、ビットロックを解除するのを忘れて出力が出ずに散々悩んでしまいました。
結局MCCの助けを借りて、PPSの設定を行ったのですが、どうもそのおかげでミスしたままのプログラムでSPIの出力が出るようになっていたようです。
ただ、この状態でもSPIモードの設定がまずく、通信できずに悩んでしまいました。
まずやったことですが、手っ取り早く、SPIをテストするために、温度センサーのADT7310を使ってみました。しかし、返ってくる値が0ばかりで全く測定できず。念のため、Arduinoでつないでみると、こちらも最初は0が返ってきました。さてはセンサーが壊れていたのかと思いましたが、こちらのブログを参考に、プルアップ抵抗を繋いでみるとうまく温度が測れました。
www.wsnak.com
なんだそんなことかと、PICの回路にプルアップ抵抗を入れてみたもののこちらは依然として0しか返ってこない。こちらのブログを参考に、センサーをBMP280に変えてみたもののやっぱり0しか返ってこない。
rikeden.net
PICから出力が出てないのではという結論に至り(この結論はこの時点では多分正しい)、Arduinoをスレーブ側にしてPICからの出力をモニターしてみました。
こちらのサイトを参考にしました。
tomosoft.jp
Arduino側でSPIを受信すると割り込みがかかるはずが、かからない。やはり出力がおかしいのではと、今度は、Arduino同士で通信すると正常に通信できている。
(この時点でMCCを一度使っているので実は出力は出ていたはず)。
ここで、原点に立ち返って、SPI通信をもう一度勉強し直し。ようやくモードが違っているのではと気づく。

SPIのモードとは

Arduino関係の記事にはモードについて述べられている記事が少なく、先の参考サイトでも何も書かれておらず、どのモードで通信しているのかわかりませんでした。実は、Arduinoは何も設定しないとモード0と呼ばれるモードで通信するのだそうです。
SPIの通信モードはモード0から3まで4種類あり、また、データを読み込むタイミングがクロックの終わりの方か、中ほどかで2種類あるので、計8種類のモードがあります。
通信モードとその動作については、こちらのサイトに詳しく書いてあります。
stupiddog.jp
PICでの設定ですが、CKEとCKPの2つのビットでモードを設定します。CKEはSSPxSTATの第6ビットにあり、CKPはSSPxCON1の第4ビットにあります。別々のレジスタにあってややこしいです。なんでこんなことをするのかよくわかりませんが、これらのレジスタはI2C通信の時にも使うのでいろいろ都合があるのかもしれません。
CKPはクロックのアイドル状態をHighかLowかを指定するビットで、1ならHighがアイドル、0ならLowがアイドルです。
CKEはクロックがアイドルからアクティブになるときにデータ転送するのか、アクティブからアイドルになるときにデータ転送するのかを決めるビットで、1なら、アクティブからアイドルになるとき、0ならアイドルからアクティブになるときとなります。
SPIモードとの対応は、下記のようになります。
SPIモード   CKE  CKP
  0     1   0
  1     0   0
  2     1   1
  3     0   1
CKEとCKPを2進数に見立てても見事に一致してませんね、とてもややこしい。
もう一つ、サンプリングの位置ですが、これは、SMPというビットで定義します。SMPはSSPxSTATの第7ビットにあり、SMPが1の時がサイクルの後半、0の時がサイクルの半ばということになります。こちらは、だいたいサイクルの半ば(つまりSMPを0にする)で行うことが多いようです。
センサーを動かすサンプルを作った時に、センサーはモード3で動かしていたので、Arduinoとの通信を行うときも、モード3を設定してしまい、それで通信がうまくいきませんでした。モードを0に設定しなおすことでようやく通信ができるようになりました。
プログラムの全文です。なお、モード0に設定しなおして、うまく動いたプログラムは、SSPの設定をミスって、ロック解除をしてませんでしたが、下記のプログラムでは修正してあります。

#include <xc.h>

// CONFIG1
#pragma config FEXTOSC = OFF    // External Oscillator mode selection bits->Oscillator not enabled
#pragma config RSTOSC = HFINT1    // Power-up default value for COSC bits->HFINTOSC (1MHz)
#pragma config CLKOUTEN = OFF    // Clock Out Enable bit->CLKOUT function is disabled; i/o or oscillator function on OSC2
#pragma config VBATEN = OFF    // VBAT Pin Enable bit->VBAT functionality is disabled
#pragma config LCDPEN = OFF    // LCD Charge Pump Mode bit->LCD Charge Pump is disabled.
#pragma config CSWEN = ON    // Clock Switch Enable bit->Writing to NOSC and NDIV is allowed
#pragma config FCMEN = ON    // Fail-Safe Clock Monitor Enable bit->FSCM timer enabled

// CONFIG2
#pragma config MCLRE = ON    // Master Clear Enable bit->MCLR pin is Master Clear function
#pragma config PWRTE = OFF    // Power-up Timer selection bits->PWRT disable
#pragma config LPBOREN = OFF    // Low-Power BOR enable bit->ULPBOR disabled
#pragma config BOREN = ON    // Brown-out reset enable bits->Brown-out Reset Enabled, SBOREN bit is ignored
#pragma config BORV = LO    // Brown-out Reset Voltage Selection->Brown-out Reset Voltage (VBOR) set to 1.9V on LF, and 2.45V on F Devices
#pragma config ZCD = OFF    // Zero-cross detect disable->Zero-cross detect circuit is disabled at POR.
#pragma config PPS1WAY = OFF    // Peripheral Pin Select one-way control->The PPSLOCK bit can be set and cleared repeatedly by software
#pragma config STVREN = ON    // Stack Overflow/Underflow Reset Enable bit->Stack Overflow or Underflow will cause a reset

// CONFIG3
#pragma config WDTCPS = WDTCPS_31    // WDT Period Select bits->Divider ratio 1:65536; software control of WDTPS
#pragma config WDTE = OFF    // WDT operating mode->WDT Disabled, SWDTEN is ignored
#pragma config WDTCWS = WDTCWS_7    // WDT Window Select bits->window always open (100%); software control; keyed access not required
#pragma config WDTCCS = SC    // WDT input clock selector->Software Control

// CONFIG4
#pragma config BBSIZE = 512    // Boot Block Size Selection bits->Boot Block Size (Words) 512
#pragma config BBEN = OFF    // Boot Block Enable bit->Boot Block disabled
#pragma config SAFEN = OFF    // SAF Enable bit->SAF disabled
#pragma config WRTAPP = OFF    // Application Block Write Protection bit->Application Block NOT write-protected
#pragma config WRTB = OFF    // Boot Block Write Protection bit->Boot Block NOT write-protected
#pragma config WRTC = OFF    // Configuration Register Write Protection bit->Configuration Words NOT write-protected
#pragma config WRTD = OFF    // Data EEPROM Write Protection bit->Data EEPROM NOT write-protected
#pragma config WRTSAF = OFF    // Storage Area Flash Write Protection bit->SAF NOT write-protected
#pragma config LVP = OFF    // Low Voltage Programming Enable bit->High Voltage on MCLR/Vpp must be used for programming

// CONFIG5
#pragma config CP = OFF    // UserNVM Program memory code protection bit->UserNVM code protection disabled

#define _XTAL_FREQ 1000000

#define CS RC3

void main(void) {
    ANSELA = 0b00000000;  
    ANSELB = 0b00000000;
    ANSELC = 0b00000000;
    TRISA  = 0b00000000;  
    TRISB = 0x00;
    TRISC  = 0b10000100;  //RC7, RC2をINPUTに
    
    // NOSC HFINTOSC; NDIV 1; 
    OSCCON1 = 0x60;
    // CSWHOLD may proceed; SOSCPWR Low power; 
    OSCCON3 = 0x00;
    // MFOEN disabled; LFOEN disabled; ADOEN disabled; SOSCEN enabled; EXTOEN disabled; HFOEN disabled; 
    OSCEN = 0x08;
    // HFFRQ 1_MHz; 
    OSCFRQ = 0x00;
    // MFOR not ready; 
    OSCSTAT = 0x00;
    // TUN 0; 
    OSCTUNE = 0x00;
    // ACTUD enabled; ACTEN disabled; 
    ACTCON = 0x00;
    
    PPSLOCK = 0x55;
    PPSLOCK = 0xAA;
    PPSLOCKbits.PPSLOCKED = 0;
    SSP1DATPPS = 0x12;              // MISO : RC2
    RB0PPS = 0x13;                  // SCK : RB0
    RC4PPS = 0x14;                  // MOSI : RC4
    PPSLOCK = 0x55;
    PPSLOCK = 0xAA;
    PPSLOCKbits.PPSLOCKED = 1;
    
    SSP1STAT = 0b01000000;  //SMP: middle/ CKE: 1
    SSP1CON1 = 0b00100000;  //SSPEN/ CKP: 0/ SPIMaster Fosc/4
    // CKE=1, CKP=0でモード0
    
    char num = 10;
    while(1){
        CS = 0;              // CSを「0」にする
        SSP1BUF = num;    // データの送信を開始
        while(!SSP1IF);          // 送信完了を待つ
        SSP1IF=0;
        CS = 1;
        RA0 = 1;
        __delay_ms(500);
        RA0 = 0;
        __delay_ms(500);
    }
    return;
}

初期設定以外は非常にシンプルです。コンフィグレーション設定と、IOポートの設定まではいつもとほぼ同じです。今回は、RC2をSPIの入力(MISO)に使うので、TRISCの第2ビットを立てています。第7ビットが立っているのはEUSARTの設定の残りです。気にしないでください。
SPIの設定は、モード0に設定するために、CKEを1、CKPを0にしてあります。MSPは0です。
そして、PPSの設定です。おまじない
PPSLOCK = 0x55;
PPSLOCK = 0xAA;
に続いて、PPSロックビット解除を行い(PPSLOCKbits.PPSLOCKED = 0;)
続いて、PPSの設定を行っています。設定の詳細は下記に記載しました。設定が終われば、またおまじないを書いて、今度はロックビットを立てます。
無限ループの中は、Lチカに加えて、SPIで10を繰り返して出力するようになっています。まず、CSを0にして、スレーブ側に通信開始を通知します。SSP1BUFにデータ(10)を書き込んで、SSP1IFをモニターして、1になる(送信終了)を待ちます。送信が終了したらSSP1IFをクリアして、CSを1にして、Lチカをやってを繰り返します。

SPI通信に使用する端子

SPI通信は、クロック(CLK)、マスターからスレーブ側の通信(MOSI)、スレーブからマスター側への通信(MISO)、チップセレクト(CS)の4端子で行います。
通信の手順は、まずCSをLowにして、スレーブに通信開始を伝えます。そして、マスター側からクロックを送り、それに応じて、マスター、スレーブ双方からデータをシリアルで送ります。従ってスレーブ側からのデータは通信終了と同時にPICの側にデータが送られてきています。PICでは、MSPPがこの機能を担い、PIC側がマスターの場合、SSPxBUFにデータを与えると直ちに通信が始まり、通信が終了した時点でSSPxBUFにスレーブ側からのデータが入っています。今回はマスター側からの送信のみをテストしたので、受信したデータのチェックは行っていません。Arduinoからは、同じデータを返すようになっています。でも同時に返すことはできるのかな? まだ、理解が不十分なようです。
センサーとかだと、コマンドを送って、その時の返ってきたデータは捨てて、次にダミーのコマンドを送ってセンサーからのデータを読み込むという操作をしています。
今回は、CS以外の端子は、SSPを使ってPIC側の端子を次のように設定しました。

SPI端子 PICポート SSP設定
CS    RC3   なし(通常の出力ポート)
CLK    RB0   RB0PPS = 0x13;
MOSI    RC4   RC4PPS = 0x14;
MISO    RC2   SSP1DATPPS = 0x12;
そして、Arduino側は、次のようになります。
CS ピン10
CLK ピン13
MOSI ピン11
MISO ピン12
従って、PICとArduinoの接続は次のようになります。
PIC  Arduino
RC3  ピン10
RB0  ピン13
RC4  ピン11
RC2  ピン12
全体の配線です。

PICとArduinoでSPI通信するときの接続

図には示していませんが、PIC側の電源はArduino側からとりました。Arduinoの5VとGNDをブレッドボードの+ラインと-ラインに繋いでいます。


そして、参考サイトのArduino側のプログラムを少し変えて、SPIのデータを受信したら、シリアルポートに出力するようにしてあります。
Arduino側のスケッチです。

/**********************************************
  Arduino uno SPI(sleave)
**********************************************/
#include <SPI.h>
 
  byte rxdata;
  int flg = 0;
 
void setup() {
  Serial.begin(9600);
  Serial.println("/-----START Arduino uno-----/");
  Serial.println("/----------SPI Slave---------/");
 
  SPCR |= bit(SPE);
  pinMode(MISO, OUTPUT);
  SPI.attachInterrupt();
}
 
void loop() {
  
  if (flg==1){
    Serial.println(rxdata);
    flg = 0;
  }
  
}
 
ISR(SPI_STC_vect) {
  rxdata = SPDR;
  SPDR = rxdata;
  flg = 1;
}

割り込み処理ルーチンで変数flgを立てて、割り込みがあったことをメインルーチンに知らせるようにしています。割り込み処理ルーチン中でシリアル出力をすると動作がおかしくなるそうです。ループ関数の中で変数flgを見て、割り込みがあったらシリアルにデータを出力するようにしています。

これで、Arduinoのシリアルポートから10が連続して出力されました。
ようやく難関のSPIにケリがついたので、次はようやくPICで電子ペーパーを動かす試験をやってみます。