第31回 温湿度・気圧センサー(動作確認プログラム)

今回は、センサーモジュールのIDを取得するプログラムを作成します。

目次

動作確認プログラムの処理内容

今回の記事では、作成したSPI通信の関数を使用して、0x0D番地の値を読み取ります。

センサーの0x0D番地の値は固定で0x60です。

この値が正しく読み取れるか、動作確認用のプログラムを作成します。


読み取った値はLCDモジュールに文字情報として表示したいところですが、この段階ではまだLCDモジュールのプログラムは作成していません。

そこで、2色のLEDを使用して結果を確認したいと思います。

結果は次のような表示にしてみます。

取得した値LEDの状態
0x60青色で点灯
0x60以外赤色で点灯

それでは、最初に動作確認プログラムの全体構成を確認してから、プログラムの中身を確認していきましょう!

動作確認プログラムの全体構成

今回作成するプログラムの構成は、次のようにしました。

Pic practice 31 program structure

プログラムは、コメントを除いて次のブロックに分かれています。

  • コンフィグレーション
    PIC16F18857のハードウェアなどの動作設定部分です。
  • ヘッダ部
    名前定義や関数のプロトタイプ宣言などです。
  • メイン関数
    センサーのIDを取得する処理部分です。
  • SPI通信関数
    SPI通信で8ビットの送信と受信を行う関数を作成します。

それぞれの部分を詳しく確認していきますが、説明の関係で、 ➡︎ ➡︎ ➡︎ の順番で進めます。

❶ コンフィグレーション

PIC16F18857のコンフィグレーション設定項目数はかなりあります。

全ての詳細を説明するとかなり長くなってしまいますので、PIC12F1822と同じ設定項目は説明を省略し、その他の設定項目については重要なもの(=設定値の確認が必要なもの)のみ説明します。

その他の設定項目は一般的な使い方をする分にはデフォルトのままで問題ありません。

クロック設定関連

PIC12F1822のクロック周波数の設定は、コンフィグレーション設定ではクロック信号源(内部か外部かなど)を選択しました。

また、信号源として内蔵クロックを選択した場合、動作周波数はプログラム内でOSCCONレジスタで設定していました。


PIC16F18857のクロック設定は、少し考え方が異なります。

コンフィグレーション設定で内蔵クロックを選択する場合、動作周波数も選択できるようになっています。

クロック設定は「RSTOSC」というレジスタで設定します。

RSTOSCは「Reset OSCillator」の略で、「リセット時/電源投入時のクロック」という意味です。リセットや電源投入時のデフォルトの周波数、というような意味合いです。

このレジスタに対して、次の設定値が用意されています。

設定値設定名称意味
111EXT1X外部クロック
110HFINT1内部クロック(1MHz)
101LFINT内部クロック(低周波)
100SOSC外部クロック(32kHz)
011
010EXT4X外部クロックの4倍
001HFINTPLL内部クロック(32kHz・PLL使用)
000HFINT32内部クロック(32kHz)

この表からわかり通り、内部クロックは1MHzか32MHzを選択することができます。(表中の太字)

今回製作するシステムは4MHzで動作させたいのですが、設定値の中に4MHzの設定はありません。(4MHzにする理由は、I2C通信プログラムを作成するときに説明します)

このような場合、クロックのコンフィグレーション設定では「内部クロック(1MHz)」(HFINT1)を選択しておき、プログラム内で4MHzに設定を変更することになります。

ただ、プログラム内でクロック周波数を変更する場合、注意が必要です。

このクロック設定とは別に、クロックのコンフィグレーションで設定した値の変更を許可するかどうかのコンフィグレーション設定があります。

この設定はCSWENレジスタで行います。CSWENはClock Switch Enableの略で、「クロックの変更許可」という意味です。

このレジスタは1ビットで次の設定内容になります。

設定値設定名称意味
1ONプログラムで周波数の変更を許可する
0OFFプログラムで周波数の変更を許可しない

プログラム中で4MHzに設定しますので、このレジスタはON(=1)に設定します。


さらにちょっとわかりづらいのですが、内部クロックを使用する場合、他のレジスタで「外部クロックを使用しない」という設定が必要です。(う〜ん、かなりややこしいですね…!)

この設定はFEXOSCレジスタで行います。FEXOSCはExternal Oscillator mode selectionの略で、「外部オシレータモード」という意味です。 (FEXOSCの最初のFの意味は不明です…)

設定値設定名称意味
111ECH外部クロック(高速)
110ECM外部クロック(中速)
101ECL外部クロック(低速)
100OFF外部クロックを使用しない
(RA7ピンは外部クロック入力ではなく、通常のGPIOピンとして使用する)
011
010HS内部クロック(高速)
001XT内部クロック(中速)
000LP内部クロック(低速)

ウォッチドッグタイマー

ウォッチドッグタイマーはPIC12F1822と同様、WDTEレジスタで設定します。

ウォッチドッグタイマーは使用しませんので、必ずOFFに設定します。(WDTEについては基礎編第21回に解説しています)


これらの点に注意して、コンフィグレーションは以下のように設定してみました。

手で入力するのは面倒ですので、基礎編第21回と同様に、MPLABX IDEのアプリケーションメニューの「Window」→「PIC Memory Views」→「Configuration Bits」の機能を使用してコード生成しています。

<div class="hcb_wrap">
<pre class="prism line-numbers lang-cpp" data-lang="C++"><code>// 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 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 = OFF      // Master Clear Enable bit (IO)
#pragma config PWRTE = ON       // Power-up Timer Enable bit (PWRT enabled)
#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 = 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)
//
// 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 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 = ON         // Low Voltage Programming Enable bit (Low Voltage programming enabled. MCLR/Vpp pin function is MCLR.)
//
// CONFIG5
#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)</code></pre>
</div>

❷ ヘッダ部

ヘッダ部は、ヘッダファイル読み込み、名前定義、関数プロトタイプ宣言、クロック周波数の_XTAL_FREQ定義をします。

インクルードファイル

インクルードファイルは、デフォルトのxc.h以外に、int_tの変数型を使用しますので「stdint.h」もインクルードします。

//
// ヘッダファイル
//   int_t型を使用するため、stdint.hをインクルード
//
#include <xc.h>
#include <stdint.h>

名前定義

SPI通信で使用するピンや、LEDに接続したピンを制御するとき、ポート名を使用するとプログラムがわかりづらくなりますので、次のように定義しました。

//
// 名前定義
//
// SPIピン名前定義
#define SPI_SCK   LATCbits.LATC7  // クロック
#define SPI_MISO  PORTCbits.RC6   // データ(センサーからPICマイコン)
#define SPI_MOSI  LATCbits.LATC5  // データ(PICマイコンからセンサー)
#define SPI_CSB   LATCbits.LATC4  // チップセレクト

// LEDピン名前定義
#define LED_RED   LATBbits.LATB0  // 赤
#define LED_BLUE  LATBbits.LATB1  // 青

関数プロトタイプ宣言

前回の記事で作成した関数、spiSend8bit()spiReceive8bit()のプロトタイプ宣言をします。

また、今回の記事ではSPI通信で1バイトデータを読み取る関数(spiRead1ByteData)を作成しますので、そのプロトタイプ宣言も行います。

//
// 関数プロトタイプ宣言
//
// SPI通信制御関数
uint8_t  spiRead1ByteData(uint8_t address);  // 指定したアドレスのデータを読み取る
void     spiSend8bit(uint8_t data);          // SPI通信8ビット送信
uint8_t  spiReceive8bit(void);               // SPI通信8ビット受信

_XTAL_FREQ定義

__delay関数を使用しますので、_XTAL_FREQで動作周波数を定義します。

今回のクロック周波数は4MHzですので以下のように設定します。

// クロック周波数
// __delay_ms(), __delay_us()関数が時間基準に使用する
// 4MHz
#define _XTAL_FREQ 4000000

ヘッダ部は以上です。(ヘッダ部でもお腹いっぱい、という感じですね…トホホ)

❹ SPI通信関数

「❸ メイン関数」の処理で「❹ SPI通信関数」を使用しますので、先にSPI関数を確認します。


前回の記事で、SPI通信の8ビット送信と8ビット受信の関数を作成しました。

通信手順では以下の部分になります。(クリックすると拡大できます)

センサモジュールBME280では、上の通信手順で、読み取りたいアドレスを1バイト送信するとそのアドレスのデータが1バイト返ってきます。

そこで、次の赤枠部分の関数を作成します。(クリックすると拡大できます)

関数は以下のように引数としてアドレス、返り値としてそのアドレスのデータにします。

uint8_t spiRead1ByteData( uint8_t address );

関数内の処理は、最初にCSB信号を0にしてセンサーを通信先に指定します。

次にspiSend8bit()でアドレスを送信、spiRead8bit()でセンサーモジュールからのデータを読み取ります。

なお、spiSend8bit()の処理のあとに、BME280がデータを準備する時間待ちをしていますが、これは念のために入れています。

プログラムは次のように作成してみました。

//
// SPI通信で指定アドレスから1バイト読み込み
// 
uint8_t spiRead1ByteData(uint8_t address) {

    // 受信データ格納変数
    uint8_t data;

    // チップセレクトを0にしてセンサーモジュールとの通信開始
    SPI_CSB = 0;

    // SPI通信手順によりアドレスを送信
    spiSend8bit(address);

    // 念のためセンサーの処理待ち
    __delay_us(10);
    
    // SPI通信手順によりデータを受信
    data = spiReceive8bit();

    // チップセレクトを1にして通信終了
    SPI_CSB = 1;

    // 受信したーデータを返す
    return data;

}

❸ メイン関数

最後にメイン関数です。メイン関数では以下の処理を行います。

  • クロック周波数設定
  • ピン属性設定
  • SPI通信処理
  • LED制御

それぞれ具体的なプログラムを確認していきましょう!

❶ クロック周波数設定

クロック周波数は、周波数値の設定と分周比の設定を行います。

「分周比」とは「元の信号の何クロックを1クロックとして数えるか」という意味です。例えば元のクロック信号の4クロックを1クロックとして数えるようにする場合、分周比は「4:1」と表現します。分周比を4:1にすると、周波数は元のクロック信号の4分の1になります。

次にそれぞれの設定項目の設定です。

周波数値はOSCFRQレジスタのHFFREQで設定を行います。HFFREQレジスタの設定値は以下になります。

設定値周波数
111
11032MHz
10116MHz
10012MHz
0118MHz
0104MHz
0012MHz
0001MHz

分周比はOSCCON1レジスタのNDIVで行います。NDIVレジスタの設定値は以下になります。

設定値分周比
1001512 : 1
1000256 : 1
0111128 : 1
011064 : 1
010132 : 1
010016 : 1
00118 : 1
00104 : 1
00012 : 1
00001 : 1

4MHzで動作させる場合、内部クロック周波数は4MHz、分周比は1:1(分周しない)を設定します。

    // 動作周波数設定
    OSCCON1bits.NDIV = 0b0000;  // 分周1:1
    OSCFRQbits.HFFRQ = 0b010;   // 4MHz

❷ ピン属性設定

SPI通信に使用するPORT Cについて、全てのピンをデジタル制御、またRA6(MISOピン)のみ入力にして、他は出力とします。

設定はは次のようになります。

    // SPI通信用ピン属性設定
    ANSELC = 0b00000000;
    TRISC  = 0b01000000;

次に、LED制御に使用するPORT Bについては、全てのピンをデジタル制御、RB0とRB1を出力に設定します。

    // LED用ピン属性設定;
    ANSELB = 0b00000000;
    TRISBbits.TRISB0 = 0;
    TRISBbits.TRISB1 = 0;

また、2本のピンをOFF(=0V)にしておきます。

    // LEDをOFF
    LED_BLUE = 0;
    LED_RED  = 0;

❸ SPI通信処理

SPI通信処理部分では、最初に初期設定としてSCK、MOSI、CSB信号を次のように設定しておくことにします。

    // SPI信号線初期設定
    SPI_SCK  = 0;  // クロックを0
    SPI_MOSI = 0;  // 送信データを0
    SPI_CSB  = 1;  // チップセレクトを1(=無効)

特にチップセレクト信号は必ず無効(=1)にしておくことを忘れないようにします。


次に、0xD0番地のデータを読み取ります。アドレスを指定してデータを読み取る関数を作成しましたので、次のように読み取ります。

    // 0xD0に格納されているチップIDを読み取る
    uint8_t chipid = 0x0;

    chipid = spiRead1ByteData(0xD0);

これでやっと0xD0番地のデータを読むことができました!

❹ LED制御

最後に、読み取った結果に応じてLEDを制御します。

成功した場合、つまり読み取ったデータが0x60の場合は青色で点灯、それ以外の場合は赤色で点灯します。

    if( chipid == 0x60 ) {
        LED_BLUE = 1;
    } else {
        LED_RED  = 1;
    }

これで一通りのプログラムができましたが、今回はこれで動作を止めますので最後にwhileを追加します。

    // ここで動作停止
    while(1){
    }

センサーID読み取りプログラムまとめ

これまでのプログラムをまとめると次のようになります。

/*
 * BME280温湿度大気圧センサー
 *   SPI通信の動作確認用プログラム
 *   [処理内容]
 *     1. 0xD0番地のチップIDを読み取る
 *     2. IDが正しく取得できていれば青色LEDを点灯
 *     3. IDが正しく取得できなければ赤色LEDを点灯
 */

//
// PIC16F18857コンィグレーション設定
//
// 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 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 = OFF      // Master Clear Enable bit (IO)
#pragma config PWRTE = ON       // Power-up Timer Enable bit (PWRT enabled)
#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 = 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)
//
// 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 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 = ON         // Low Voltage Programming Enable bit (Low Voltage programming enabled. MCLR/Vpp pin function is MCLR.)
//
// CONFIG5
#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)


//
// ヘッダファイル
//   int_t型を使用するため、stdint.hをインクルード
//
#include <xc.h>
#include <stdint.h>


//
// 定数
//
// SPIピン名前定義
#define SPI_SCK   LATCbits.LATC7  // クロック
#define SPI_MISO  PORTCbits.RC6   // データ(センサーからPICマイコン)
#define SPI_MOSI  LATCbits.LATC5  // データ(PICマイコンからセンサー)
#define SPI_CSB   LATCbits.LATC4  // チップセレクト

// LEDピン名前定義
#define LED_RED   LATBbits.LATB0  // 赤
#define LED_BLUE  LATBbits.LATB1  // 青

//
// 関数プロトタイプ宣言
//
// SPI通信制御関数
uint8_t  spiRead1ByteData(uint8_t address);  // 指定したアドレスのデータを読み取る
void     spiSend8bit(uint8_t data);          // SPI通信8ビット送信
uint8_t  spiReceive8bit(void);               // SPI通信8ビット受信


// クロック周波数
// __delay_ms(), __delay_us()関数が時間基準に使用する
// 4MHz
#define _XTAL_FREQ 4000000


//
// main関数
//
void main(void) {

    // 動作周波数設定
    OSCCON1bits.NDIV = 0b0000;  // 分周1:1
    OSCFRQbits.HFFRQ = 0b010;   // 4MHz

    // SPI通信用ピン属性設定
    ANSELC = 0b00000000;
    TRISC  = 0b01000000;
    
    // LED用ピン属性設定;
    ANSELB = 0b00000000;
    TRISBbits.TRISB0 = 0;
    TRISBbits.TRISB1 = 0;

    // SPI信号線初期設定
    SPI_SCK  = 0;  // クロックを0
    SPI_MOSI = 0;  // 送信データを0
    SPI_CSB  = 1;  // チップセレクトを1(=無効)
    
    // LEDをOFF
    LED_BLUE = 0;
    LED_RED  = 0;

    // 0xD0に格納されているチップIDを読み取る
    uint8_t chipid = 0x0;

    chipid = spiRead1ByteData(0xD0);
    
    if( chipid == 0x60 ) {
        LED_BLUE = 1;
    } else {
        LED_RED  = 1;
    }
    
    // ここで動作停止
    while(1){
    }

}


//
// SPI通信で指定アドレスから1バイト読み込み
// 
uint8_t spiRead1ByteData(uint8_t address) {

    // 受信データ格納変数
    uint8_t data;

    // チップセレクトを0にしてセンサーモジュールとの通信開始
    SPI_CSB = 0;

    // SPI通信手順によりアドレスを送信
    spiSend8bit(address);

    // 念のためセンサーの処理待ち
    __delay_us(10);
    
    // SPI通信手順によりデータを受信
    data = spiReceive8bit();

    // チップセレクトを1にして通信終了
    SPI_CSB = 1;

    // 受信したーデータを返す
    return data;

}


//
// SPIデータ8ビット書き込み
//   SCK/MOSI制御のための関数であるため
//   チップセレクト信号はこの関数の前後で制御すること
void spiSend8bit(uint8_t data) {

    // 8ビット分繰り返す
    for (int8_t i=7; i>=0; i--) {

        // (1)クロックを0にする
        SPI_SCK = 0;
        
        // (2)MOSIにデータをセットする
        if( data & (1<<i) ) {
            SPI_MOSI = 1;
        } else {
            SPI_MOSI = 0;
        }
      
        // (3)クロックを1にする
        SPI_SCK = 1;
    }

}

//
// SPIデータ8ビット読み込み
//   SCK/MOSI制御のための関数であるため
//   チップセレクト信号はこの関数の前後で制御すること
//
uint8_t spiReceive8bit() {

    // 受信データ格納変数
    uint8_t read_data = 0;

    // 8ビット分繰り返す
    for (int8_t i=7; i>=0; i--) {

        // 受信データ変数を1ビット左シフト
        read_data <<= 1;

        // (1)クロックを0にする
        SPI_SCK = 0;
        
        // (2)クロックを1にする
        SPI_SCK = 1;

        // (3)この時点でセンサからのデータを読めるので、MISOのピン状態を読む
        if(SPI_MISO){
            read_data |= 1;
        }
    }

    // 受信したデータを返す
    return read_data;

}

次回は作成したプログラムで動作確認をします。

更新履歴

日付内容
2018.5.6新規投稿
2025.7.31プログラム処理内容を変更(結果をLEDの色で表現)
通知の設定
通知タイミング
guest
4 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
マイコン入門
マイコン入門
3 年 前

2点ご質問です。
1点目
8ビットデータ受信関数内で、if分でSPI_MISOが真ならば(何か値があれば?)read_dataと1の論理和をとるとのことですが、SPI_MISOには0x60のデフォルト値が入っているので常に真となり、forの繰り返し後はread_dataは11111111となってしまわないのでしょうか?
2点目
データ送受信でマスター側が送ったアドレスデータを受信したスレーブは、どのようにしてそのアドレスとアドレス内のデータを紐づけているのでしょうか。
初学者であるため質問の内容が分かりづらいかもしれませんが、ご回答よろしくお願いいたします。

マイコン入門
マイコン入門
返信  管理者
3 年 前

ご丁寧にありがとうございます。
[スレーブとアドレスのデータの紐づけ]は仕様なのですね。理解しました。
[read_dataが0b11111111になるのではないか?]
についてもう少しご質問させてください。
SPI_MISOが1ビットデータであるとのことなのですが、SPI_MOSIも同じく1ビットデータということでしょうか。
私のイメージとしては、spiSend8bit関数でデータのアドレスをクロックの立ち上がりのタイミングでMOSIの1ビットデータとしてマイコンからセンサへと送る。(読み書きビットと合わせて合計8ビット分)

センサ側では送られてきた1ビットデータ8個分がMISOの1ビットデータ8個分としてクロックの立ち上がりで読み込める状態となっている。
→spiReceive8bit関数でMISOデータを1ビットずつ読みだして、read_dataの8ビットデータ(ID)として返す。
というイメージであっていますでしょうか。

その場合気になる点は、MOSIで1ビットデータを繰り返し8ビット送っていますが、その1ビットデータはMOSIとして8回上書きされることにはならないのでしょうか。その場合、繰り返して上書きされた7ビット目のデータをMISO側で何度も読み直してしまわないのかと考えて、先日の質問に至った次第です。

それとも仕様で、送ったMOSIデータは1ビットずつ別々のMISOデータとしてセンサ側に格納されて、spiReceive8bit関数ではそのデータを順々に取り出しているみたいな感じでしょうか。(ライブラリを使用したshiftOutでは、Wire.read()で順番に読みだしていたような気がします。)

長文で申し訳ありませんが、どうぞよろしくお願いいたします。

目次