多くの PIC に搭載されている MSSPモジュールを使うと I2C と SPI の2つのシリアル通信を行うことができます。
今回は SPI通信のやり方(マスター)を紹介します。
I2C についてはこちらを参照してください。
SPI通信とは
まず初めに SPI通信について軽く紹介します。
SPI通信ではマスターとスレーブがそれぞれシフトレジスタを持っていて、クロックに合わせてその値を交換します。
通信に必要な線は、INPUT、OUTPUT、クロックの3本と、スレーブ1つにつき1本のスレーブ選択用の線となります。
スレーブが複数あっても2本で通信できる I2C に比べて、多くの線が必要となるのはデメリットと言えます。
では、SPI のメリットは何かというと、通信速度の速さが挙げられます。I2C のクロック周波数は主に 100kHz や 400kHz ですが、SPI では 1MHz 以上(PIC のクロックに依存しますがもっと)出すことができます。
センサーなどのほかに通信量の多い液晶や SDカードとの通信などに使われます。
SPI通信の種類
SPI通信を行うときに注意しなくてはいけないのは、SPI通信には次のようにいくつもの種類があるということです。
- クロックのアイドル状態:Low / High
- 送信タイミング:Idle to Active / Active to Idle
- 受信タイミング:中央 / 後縁
統一されていれば楽なのですが、デバイスに合わせて設定しなければいけません。いきなりこれを見てもよく分からないと思うので、具体的な事例で確認してみましょう。
今回はこちらの温湿度・気圧センサーBME280を使用します。データシートを確認してみると、SPI通信のタイミングチャートが載っていました。
この図から次のことがわかります。
- クロックのアイドル状態:High
- 送信タイミング:Active to Idle
- 受信タイミング:中央
このようにデータシートを確認して通信の種類を決定しましょう。
SPI制御レジスタ
続いて、SPI通信を制御するためのレジスタについて説明します。MSSPモジュールは I2C と SPI の両方をサポートしていますが、今回は SPI のマスターに関わる部分だけ説明します。
以下、レジスタ名に “x” が登場しますが、これは MSSPモジュールを複数搭載している場合にその総称として用いています。
実際に使用するときには、1または2、MSSPモジュールが一つだけの場合は何も無いものとして解釈してください。
SSPxSTAT
受信サンプル位置
1:後縁 0:中央
CKE (SPI Clock Edge Select bit)
送信エッジ
1:Active to Idle 0:Idle to Active
BF (Buffer Full Status bit)
1:SSPxBUF にデータあり 0:データなし
SSPxSTATレジスタでは、クロックの設定とバッファフル状態の確認ができます。他のビットは I2C のときのみ使用するビットです。
SSPxCON1
1:Write衝突発生 0:正常SSPOV (Receive Overflow Indicator bit)
1:受信オーバーフロー発生 0:正常
SSPEN (Synchronous Serial Port Enable bit)
1:SSP有効化 0:無効化
CKP (Clock Polarity Select bit)
クロックのアイドル状態
1:High 0:Low
SSPM (Synchronous Serial Port Mode Select bits)
1010:SPIマスター Clock = FOSC/(4 * (SSPxADD+1))
0011:SPIマスター Clock = TMR2/2
0010:SPIマスター Clock = FOSC/64
0001:SPIマスター Clock = FOSC/16
0000:SPIマスター Clock = FOSC/4
SSPxCON1レジスタでは SPIのモードなどを設定します。最初の二つのビットは使わないで大丈夫です。クロックの周波数は迷ったら一番速い 0000 にしておけば大丈夫だと思います。
SSPxBUF
SSPxBUFレジスタは送受信に使うバッファです。
このレジスタに値を書き込むことでその値が送信され、同時に受信した値はこのレジスタを読むことで取得できます。
サンプルプログラム
続いて、先程のレジスタを使用するサンプルプログラムを紹介します。
まずは、初期化と1バイトを交換する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void SPI_Init(){ SSP1STAT = 0b00000000; //SMP: middle/ CKE: IdleToActive SSP1CON1 = 0b00110000; //SSPEN/ CKP: Idle High/ SPIMaster Fosc/4 } char SPI_Exchange(char data){ char dumy; dumy = SSP1BUF; //Clear buffer SSP1BUF = data; //Send while(!SSP1STATbits.BF); return SSP1BUF; //Receive } |
SPI の設定は今回使用する BME280 に合わせています。
交換の関数ですが、引数を送信し、受信した値を戻り値として返します。送信のみの場合は戻り値を無視し、受信のみの場合はダミーのデータを送信します。
次に、BME280 の通信プロトコルを確認してみましょう。
書き込み
読み出し
これを参考にして作ったコマンド書き込みとデータ読み出しの関数がこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void BME_SPI_Cmd(char add, char data){ CS = 0; SPI_Exchange(add&0x7F); SPI_Exchange(data); CS = 1; } void BME_SPI_Get(char* buf, char add, char count){ CS = 0; SPI_Exchange(add|0x80); for(char i=0; i<count; i++){ buf[i] = SPI_Exchange(0); } CS = 1; } |
CSはチップセレクタピンで、#define で指定しておきます。
I2C と異なり、SPI ではスレーブアドレスを送信する必要がない代わりに、Read/Write をレジスタアドレスの先頭ビットに指定する必要があるようです。
動作確認
それではこのプログラムを使用して、温度を読み取って LCD に表示させてみたいと思います。今回使用するものは以下の通りです。
- PIC16F18857
- 温湿度・気圧センサーBME280
- I2C接続LCD
LCD に I2C を使うので、センサーも I2C を使えばいいのですが、今回は SPI の動作確認なので目を瞑りましょう。
プログラムは以下の通りです。I2C、LCD、センサーの使い方はそれぞれリンク先のページで説明しているため、ここでは省略します。必要に応じて参照ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <xc.h> void SPI_Init(){ SSP1STAT = 0b00010000; //SMP: middle/ CKE: ActiveToIdle SSP1CON1 = 0b00110000; //SSPEN/ CKP: Idle High/ SPIMaster Fosc/4 SSP1DATPPS = 0x14; // SDI : RC4 RC3PPS = 0x14; // SCK : RC3 RC5PPS = 0x15; // SDO : RC5 } char SPI_Exchange(char data){ char dumy; dumy = SSP1BUF; //Clear buffer SSP1BUF = data; //Send while(!SSP1STATbits.BF); return SSP1BUF; //Receive } |
1 2 |
void SPI_Init(); char SPI_Exchange(char data); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
#pragma config FEXTOSC = OFF // External Oscillator mode selection bits (Oscillator not enabled) #pragma config RSTOSC = EXT1X // Power-up default value for COSC bits (EXTOSC operating per FEXTOSC bits) #pragma config CLKOUTEN = OFF // Clock Out Enable bit (CLKOUT function is disabled; i/o or oscillator function on OSC2) #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) #pragma config MCLRE = ON // Master Clear Enable bit (MCLR pin is Master Clear function) #pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled) #pragma config LPBOREN = OFF // Low-Power BOR enable bit (ULPBOR disabled) #pragma config BOREN = OFF // Brown-out reset enable bits (Brown-out reset disabled) #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 = ON // Peripheral Pin Select one-way control (The PPSLOCK bit can be cleared and set only once in software) #pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a reset) #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) #pragma config WRT = OFF // UserNVM self-write protection bits (Write protection off) #pragma config SCANE = available// Scanner Enable bit (Scanner module is available for use) #pragma config LVP = OFF // Low Voltage Programming Enable bit (High Voltage on MCLR/Vpp must be used for programming) #pragma config CP = OFF // UserNVM Program memory code protection bit (Program Memory code protection disabled) #pragma config CPD = OFF // DataNVM code protection bit (Data EEPROM code protection disabled) #include <xc.h> #include <stdio.h> #include "i2c.h" #include "lcd_i2c.h" #include "spi.h" #define _XTAL_FREQ 4000000 #define CS RC2 void Init(){ ANSELA = 0; TRISA = 0; ANSELB = 0; TRISB = 0b00000110; ANSELC = 0; TRISC = 0b00010000; } void putch(char c){ lcd_DATA(c); } void BME_SPI_Cmd(char add, char data){ CS = 0; SPI_Exchange(add&0x7F); SPI_Exchange(data); CS = 1; } void BME_SPI_Get(char* buf, char add, char count){ CS = 0; SPI_Exchange(add|0x80); for(char i=0; i<count; i++){ buf[i] = SPI_Exchange(0); } CS = 1; } void main(void) { Init(); I2C_Master_Init(100000); lcd_Init(); SPI_Init(); CS = 1; char buf[2]; unsigned int data; int T; //Initialize BME280 BME_SPI_Cmd(0xE0, 0xB6); // reset BME_SPI_Cmd(0xF2, 0x01); // Ctrl_hum_reg BME_SPI_Cmd(0xF4, 0x27); // Ctrl_meas_reg BME_SPI_Cmd(0xF5, 0xA0); // config/reg lcd_SetCursor(0,0); printf("BME 280"); while(1){ BME_SPI_Get(buf, 0xFA, 2); // Get from 0xFA to 0xFB data = (buf[0]<<8) | buf[1]; T = ((long)data*101/2000 - 1418); lcd_SetCursor(0,1); printf("%d.%d degC", T/10, T%10); __delay_ms(500); } } |
I2Cライブラリ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#include<xc.h> #define _XTAL_FREQ 4000000 void I2C_Master_Init(const unsigned long c){ SSP2CON1 = 0x28; //SSP2 Module as Master SSP2CON2 = 0x00; SSP2CON3 = 0x00; SSP2ADD = (_XTAL_FREQ/(4*c))-1; //Setting Clock Speed SSP2STAT = 0x80; SSP2CLKPPS = 0x09; //PPS Settings SSP2DATPPS = 0x0a; // SCL : RB1 RB1PPS = 0x16; // SDA : RB2 RB2PPS = 0x17; } void I2C_Master_Wait(){ while ((SSP2STAT & 0x04) || (SSP2CON2 & 0x1F)); //Transmit is in progress } void I2C_Master_Start(){ I2C_Master_Wait(); SSP2CON2bits.SEN = 1; //Initiate start condition } void I2C_Master_RepeatedStart(){ I2C_Master_Wait(); SSP2CON2bits.RSEN = 1; //Initiate repeated start condition } void I2C_Master_Stop(){ I2C_Master_Wait(); SSP2CON2bits.PEN = 1; //Initiate stop condition } void I2C_Master_Write(unsigned d){ I2C_Master_Wait(); SSP2BUF = d; //Write data to SSP2BUF } unsigned short I2C_Master_Read(unsigned short a){ unsigned short temp; I2C_Master_Wait(); SSP2CON2bits.RCEN = 1; I2C_Master_Wait(); temp = SSP2BUF; //Read data from SSP2BUF I2C_Master_Wait(); SSP2CON2bits.ACKDT = a; //Acknowledge bit 1:Not Ack 0:Ack SSP2CON2bits.ACKEN = 1; //Acknowledge sequence return temp; } void SendI2C(char adrs, char data){ I2C_Master_Start(); I2C_Master_Write(adrs<<1); //SlaveAdress + Write I2C_Master_Write(data); //mainly RegisterAdress I2C_Master_Stop(); } void CmdI2C(char adrs, char reg, char data){ I2C_Master_Start(); I2C_Master_Write(adrs<<1); //SlaveAdress + Write I2C_Master_Write(reg); //RegisterAdress I2C_Master_Write(data); I2C_Master_Stop(); } void GetDataI2C(char adrs, char* buf, char cnt){ I2C_Master_Start(); I2C_Master_Write((adrs<<1)+1); //SlaveAdress + Read for(char i=0; i<cnt-1; i++) buf[i] = I2C_Master_Read(0); //ACK buf[cnt-1] = I2C_Master_Read(1); //NACK I2C_Master_Stop(); } |
1 2 3 4 5 6 7 8 9 10 |
void I2C_Master_Init(const unsigned long c); void I2C_Master_Wait(); void I2C_Master_Start(); void I2C_Master_RepeatedStart(); void I2C_Master_Stop(); void I2C_Master_Write(unsigned d); unsigned short I2C_Master_Read(unsigned short a); void SendI2C(char adrs, char data); void CmdI2C(char adrs, char reg, char data); void GetDataI2C(char adrs, char* buf, char cnt); |
LCDライブラリ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include "i2c.h" #include <xc.h> #define _XTAL_FREQ 4000000 #define CONTRAST 0x20 //for 3.3V //#define CONTRAST 0x08 //for 5V const char lcd_DDRAM[4] = {0x00, 0x40, 0x14, 0x54}; void lcd_INST(char command){ I2C_Master_Start(); I2C_Master_Write(0x7C); //SlaveAdress + Write I2C_Master_Write(0x00); //Instruction I2C_Master_Write(command); //data byte I2C_Master_Stop(); __delay_us(30); } void lcd_DATA(char data){ I2C_Master_Start(); I2C_Master_Write(0x7C); //SlaveAdress + Write I2C_Master_Write(0x40); //Data I2C_Master_Write(data); //data byte I2C_Master_Stop(); __delay_us(30); } void lcd_Init(){ __delay_ms(40); lcd_INST(0x38); //Function Set lcd_INST(0x39); //Function Set lcd_INST(0x14); //Internal OSC Frequency lcd_INST(0x70 + (CONTRAST & 0x0f)); //Contrast Set lcd_INST(0x54 + ((CONTRAST & 0xf0)>>4)); //Power/ICON/Contrast control lcd_INST(0x6c); //Follower control __delay_ms(200); lcd_INST(0x38); //Function Set lcd_INST(0x0c); //Display ON/OFF control lcd_INST(0x01); //Clear Display __delay_ms(2); } void lcd_SetCursor(char x,char y){ char d = lcd_DDRAM[y] + x; lcd_INST( 0x80+d); } |
1 2 3 4 |
void lcd_INST(char command); void lcd_DATA(char data); void lcd_Init(); void lcd_SetCursor(char x,char y) |
PIC16F18857 では SDO と SCK のピンがデフォルトで割当て先が決まっていないので PPS の設定をしておきます。
また、I2C では SCL や SDA のピンは入力にしますが、SPIでは普通の感覚で SCK と SDO は出力、SDI は入力にします。
回路図はこんな感じです。
無事に動作確認できました。暑いですね…
この記事は以上となります。ご覧いただきありがとうございました。