今回は、センサモジュールのIDを取得するプログラムを作成します。
確認プログラム構成
今回作成するプログラムの構成は以下のようになっています。プログラムの中身の説明に入る前に、全体の構成を把握します。
プログラムは、コメントを除いて「コンフィグレーション」#defineやプロトタイプ宣言などの「ヘッダ部」「メイン関数」「SPI通信関数」です。それぞれのパートを説明します。
PIC16F18857のコンフィグレーション
PIC16F18857のコンフィグレーション設定項目は、PIC12F1822に比べてかなり多いです。全て説明するとかなり長くなってしまいますので、PIC12F1822と同じ設定項目は説明を省略し、その他の設定項目については重要なもの(=設定値の確認が必要なもの)のみ説明します。その他の設定項目は一般的な使い方をする分にはデフォルトのままで問題ありません。
クロック設定関連
PIC12F1822のクロック周波数の設定は、コンフィグレーションでクロック信号源の選択を行い、内蔵クロックを選択した場合の実際の周波数はプログラム内でOSCCONレジスタで設定していました。
PIC16F18857のクロック設定は、ちょっとだけ考え方が異なります。コンフィグレーション設定で内臓クロックを選択する場合、動作周波数もいくつか選択できるようになっています。
クロック設定は「RSTOSC」というレジスタで設定します。RSTOSCはReset OSCillator(リセット時/電源投入時のクロック)という意味です。このレジスタは以下の設定値が用意されています。
設定値 | 設定名称 | 意味 |
---|---|---|
111 | EXT1X | 外部クロック |
110 | HFINT1 | 内部クロック(1MHz) |
101 | LFINT | 内部低周波クロック |
100 | SOSC | 外部クロック(32kHz) |
011 | – | – |
010 | EXT4X | 外部クロックの4倍 |
001 | HFINTPLL | 内部クロック(32MHz(PLL使用)) |
000 | HFINT32 | 内部クロック(32MHz) |
内部クロックは1MHzか32MHzを選択することになっています。今回製作するシステムは4MHzで動作させます(理由ついてはI2C通信プログラムを作成するときに説明します)。表をみると4MHzの設定はコンフィグレーション設定にはありません。
そのため、クロックのコンフィグレーション設定では「内部クロック(1MHz)」(HFINT1)を選択しておき、プログラム内で4MHzに設定を変更することになります。ただ、プログラム内でクロック周波数を変更する場合、注意が必要です。
クロックのコンフィグレーションで設定した値の変更を許可するかどうかのコンフィグレーション設定があります。この設定は「CSWEN」レジスタで行います。CSWENはClock Switch Enableの略で、「クロックの変更許可」という意味です。このレジスタは1ビットで以下の設定内容になります。
設定値 | 設定名称 | 意味 |
---|---|---|
1 | ON | プログラムでの周波数変更を許可 |
0 | OFF | プログラムでの周波数変更を禁止 |
プログラム中で4MHzに設定しますので、このレジスタはON(1)に設定します。
さらにちょっとわかりづらいのですが、外部クロックを使用する場合は別のレジスタでも設定が必要で、かつ内部クロックを使用する場合もこのレジスタで「外部クロックを使用しない」という設定が必要ですので注意します。この設定は「FEXOSC」レジスタで行います。FEXOSCはExternal Oscillator mode selectionの意味です。(FEXOSCの最初のFの意味はわかりませんでした….)
設定値 | 設定名称 | 意味 |
---|---|---|
111 | ECH | 外部クロック(高速) |
110 | ECM | 外部クロック(中速) |
101 | ECL | 外部クロック(低速) |
100 | OFF | 外部クロックを使用しない。RA7ピンは外部クロック入力ではなく通常のI/Oピンとして使用する |
011 | – | – |
010 | HS | 内部クロック(高速) |
001 | XT | 内部クロック(中速) |
000 | LP | 内部クロック(低速) |
また、CLKOUTENレジスタはOFFにしておきます。CLKOUTENについては基礎編第21回に解説しております。
ウォッチドッグタイマー
ウォッチドッグタイマーはPIC12F1822と同様、WDTEレジスタで設定します。ウォッチドッグタイマーは使用しませんので、必ずOFFに設定します。WDTEについては基礎編第21回に解説しております。
これらの点に注意して、コンフィグレーションは以下のように設定します。手で入力するのは面倒ですので、基礎編第21回と同様に、MPLABX IDEのアプリケーションメニューの「Window」→「PIC Memory Views」→「Configuration Bits」の機能を使用してコード生成しています。
// 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)
ヘッダ部
ヘッダ部は、インクルードファイル、#define、関数プロトタイプ宣言、クロック周波数の_XTAL_FREQ定義をします。
インクルードファイル
インクルードファイルは、デフォルトのxc.h以外に、int_tの変数型を使用しますので「stdint.h」もインクルードします。
//
// ヘッダファイル
// int_t型を使用するため、stdint.hをインクルード
//
#include <xc.h>
#include <stdint.h>
#define
先の記事で説明しましたように、SPI通信ではPICマイコンのピン4本を制御しますが、ピンのポート名を使用するとプログラムがわかりづらくなりますので、以下のように定義します。
// SPIピン設定
#define SPI_SCK LATCbits.LATC7 // クロック
#define SPI_MISO PORTCbits.RC6 // スレーブ→マスタ
#define SPI_MOSI LATCbits.LATC5 // マスター→スレーブ
#define SPI_CSB LATCbits.LATC4 // チップセレクト
関数プロトタイプ宣言
前回の記事で作成した関数、「spiSend8bit」と「spiReceive8bit」のプロトタイプ宣言をします。また、今回の記事ではSPI通信で1バイトデータを読み取る関数(spiRead1ByteData)を作成しますので、そのプロトタイプ宣言も行います。
// SPI通信制御関数
uint8_t spiRead1ByteData(uint8_t address); // 指定したアドレスのデータを読み取る
void spiSend8bit(uint8_t data); // SPI通信手順の8ビット送信
uint8_t spiReceive8bit(); // SPI通信手順の8ビット受信
_XTAL_FREQ定義
__delay関数を使用しますので、_XTAL_FREQを定義します。PIC12F1822のプログラムと同様ですが、今回のクロック周波数は4MHzですので以下のように設定します。
// クロック周波数
// __delay_ms(), __delay_us()関数が時間基準に使用する
// 4MHz
#define _XTAL_FREQ 4000000
ヘッダ部は以上です。
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通信処理
それぞれ以下のようにプログラムを作成します。
1. クロック周波数設定
クロック周波数は、周波数値の設定と分周比の設定を行います。「分周比」は応用編で登場しましたがここでも設定が必要になります。「分周比」とは「元の信号を、何回分を1回として数えるか」という意味です。例えばクロック信号の場合、4回のクロックを1回として数えるようにする場合、分周比は「4:1」と表現します。分周比を4:1にすると、4回のクロックを1回として数えることになりますので、4分の1の周波数になります。
次に書く設定項目の設定方法を説明します。
周波数値は「OSCFRQ」レジスタの「HFFREQ」で設定を行います。HFFREQレジスタの設定値は以下になります。
設定値 | 周波数 |
---|---|
111 | – |
110 | 32MHz |
101 | 16MHz |
100 | 12MHz |
011 | 8MHz |
010 | 4MHz |
001 | 2MHz |
000 | 1MHz |
また、分周比は「OSCCON1」レジスタの「NDIV」で行います。NDIVレジスタの設定値は以下になります。
設定値 | 分周比 |
---|---|
1001 | 512:1 |
1000 | 256:1 |
0111 | 128:1 |
0110 | 64:1 |
0101 | 32:1 |
0100 | 16:1 |
0011 | 8:1 |
0010 | 4:1 |
0001 | 2:1 |
0000 | 1:1 |
4MHzで動作させる場合、動作周波数は4MHz、分周比は1:1(分周しない)を設定します。
// 動作周波数設定
OSCCON1bits.NDIV = 0b0000; // 分周1:1
OSCFRQbits.HFFRQ = 0b010; // 4MHz
2. ピン属性設定
ピンの属性設定はPIC12F1822と同様ですので詳しい説明は省略します。
ポートCについて、全てのピンをデジタル制御、またRA6(MISOピン)のみ入力にして他は出力とします。設定は以下のようになります。
// ピン属性設定
ANSELC = 0b00000000;
TRISC = 0b01000000;
SPI通信処理
SPI通信処理は、最初に初期設定としてSCK、MOSI、CSBを以下のように設定しておきます。
// SPI信号線初期設定
SPI_SCK = 0; // クロックを0
SPI_MOSI = 0; // マスタ→スレーブを0
SPI_CSB = 1; // チップセレクトを1(=無効)
特にチップセレクト信号は必ず無効(=1)にしておくことを忘れないようにします。
次に、先ほど作成したアドレスを指定して1バイト読み取る部分です。以下のように0xD0番地のデータを読み取ります。
// 0xD0に格納されているチップIDを読み取る
uint8_t chipid = 0;
chipid = spiRead1ByteData(0xD0);
これでやっと0xD0番地のデータを読むことができました。
チップID読み取りプログラム
これまでのプログラムをまとめると以下のようになります。なお、0xD0番地を読み取った後はwhile()文で動作を止めています。
/*
* BME280温湿度大気圧センサ
* SPI通信の動作確認用プログラム
* (0xD0番地のチップID(0x60)を読み取る)
*
* https://tool-lab.com/
*
* 更新
* 2018. 4.28: 新規作成
*
*/
//
// 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 // スレーブ→マスタ
#define SPI_MOSI LATCbits.LATC5 // マスター→スレーブ
#define SPI_CSB LATCbits.LATC4 // チップセレクト
//
// 関数プロトタイプ宣言
//
// SPI通信制御関数
uint8_t spiRead1ByteData(uint8_t address); // 指定したアドレスのデータを読み取る
void spiSend8bit(uint8_t data); // SPI通信手順の8ビット送信
uint8_t spiReceive8bit(); // SPI通信手順の8ビット受信
// クロック周波数
// __delay_ms(), __delay_us()関数が時間基準に使用する
// 4MHz
#define _XTAL_FREQ 4000000
//
// main関数
//
void main(void) {
// 動作周波数設定
OSCCON1bits.NDIV = 0b0000; // 分周1:1
OSCFRQbits.HFFRQ = 0b010; // 4MHz
// ピン属性設定
ANSELC = 0b00000000;
TRISC = 0b01000000;
// SPI信号線初期設定
SPI_SCK = 0; // クロックを0
SPI_MOSI = 0; // マスタ→スレーブを0
SPI_CSB = 1; // チップセレクトを1(=無効)
// 0xD0に格納されているチップIDを読み取る
uint8_t chipid = 0;
chipid = spiRead1ByteData(0xD0);
// ここで動作停止
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;
}
ミニチャレンジ課題
PICマイコンの動作周波数設定で、特定の周波数に設定したいことがあります。そこで、PIC16F18857で、500kHzの動作周波数に設定したい場合、周波数設定と分周比はどのように設定すればよいか考えてみてください。
500kHzにするための動作周波数と分周比の設定は何種類かありますが、動作周波数は周波数が高いほど消費電力が大きくなりますので、その点を考慮して決めてみましょう。
次回は動作確認プログラムで動作確認をします。
更新履歴
日付 | 内容 |
---|---|
2018.5.6 | 新規投稿 |
2点ご質問です。
1点目
8ビットデータ受信関数内で、if分でSPI_MISOが真ならば(何か値があれば?)read_dataと1の論理和をとるとのことですが、SPI_MISOには0x60のデフォルト値が入っているので常に真となり、forの繰り返し後はread_dataは11111111となってしまわないのでしょうか?
2点目
データ送受信でマスター側が送ったアドレスデータを受信したスレーブは、どのようにしてそのアドレスとアドレス内のデータを紐づけているのでしょうか。
初学者であるため質問の内容が分かりづらいかもしれませんが、ご回答よろしくお願いいたします。
ご質問どうもありがとうございます。
[read_dataが0b11111111になるのではないか?]
if文は if(SPI_MISO) と書いてありますが、#defineで定義していますので、実際には
if( PORTCbits.RC6)という意味になります。
PORTCbits.RC6は8ビットではなく、1ビットデータになります。BME280から、0x60が最上位ビットから1ビットずつ送られてきますので、それを1ビットずつ読み取り、read_dataに読み込んでいきます。
ちょっとわかりづらいですが、SPI_MISOは0x60の8ビットデータが入るわけではなく、1ビットの情報になります。
もし分かりづらいようでしたら、お手数ですがご質問いただければと思います。
[スレーブのアドレスとデータの紐付け]
これはBME280の動作仕様ということになります。
BME280の中にはメモリがあり、そのメモリに音質度、気圧データなどが格納されています。マスターからアドレスを送信すると、BME280はそのアドレスにあるメモリのデータを送信準備します。マスターから読み出しが始まるとその用意したデータをMISOピンに1ビットずつ送信していきます。
こちらも分かりづらいようでしたら、お手数ですがご質問いただければと思います。
ご丁寧にありがとうございます。
[スレーブとアドレスのデータの紐づけ]は仕様なのですね。理解しました。
[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()で順番に読みだしていたような気がします。)
長文で申し訳ありませんが、どうぞよろしくお願いいたします。
> SPI_MISOが1ビットデータであるとのことなのですが、SPI_MOSIも同じく1ビットデータということでしょうか。
はい、SPI_MISOもSPI_MOSIも#defineでPORT Cのうちの1ビットで定義していますので、1ビットとなります。またそもそもSPI通信もI2C通信もシリアル通信、つまり1回(1クロック)に1ビットずつ通信する通信手順です。詳細につきましては実践編の第22回〜26回に詳しく説明していますのでご確認いただければと思います。
spiSend8bit関数もspiReceive8bit関数も、クロックの立ち上がりで1ビットずつ通信するように作成してあります。
> その場合気になる点は、MOSIで1ビットデータを繰り返し8ビット送っていますが、その1ビットデータはMOSIとして8回上書きされることにはならないのでしょうか。
送信側も受信側も8ビットのレジスタを使ってやりとりしています。具体的には、例えば受信側の場合、1ビット受信するごとに1ビットずらしていき、8ビット分を受信します。
「その1ビットデータはMOSIとして8回上書きされることにはならないのでしょうか」についてはそうならないように設計されている、ということになります。(そもそも1ビット受け取るごとに上書きしてしまったら製品にならない、ということになってしまいます)