第53回 天気予報プログラム(最終回)

天気予報のアルゴリズムをプログラムに実装します。

目次

実装仕様

実装するアルゴリズム

前回までの記事で天気予報のアルゴリズムを検討しました。

具体的には次のような内容でした。

STEP
気圧変化の速さを判断

最初に、次のように気圧変化の速さを判断して、天気変化のトレンドを判断することにします。

過去3時間の気圧変化気圧変化の速さの判断
± 1.6hPa未満ゆるやかな変化
(現在の天気が継続する)
± 1.6Pa以上速い上昇または下降
(近いうちに天気が変化する)
STEP
現在の気圧の程度を判断

次に、現在の気圧から、高気圧帯にいるのか低気圧帯にいるのか判断します。

現在の気圧値現在の気圧帯
1015hPa以上高気圧
1000〜1015hPa普通
1000hPa未満低気圧
STEP
天気傾向の判断

ステップ1の「気圧変化の速さ」と、ステップ2の「現在の気圧帯」から、天気傾向を判断します。

現在の気圧帯気圧変化
速い上昇
気圧変化
ゆるやか
気圧変化
速い下降
高気圧
(1015hPa以上)
晴れ
(継続)
晴れ
(継続)
曇り
(悪化傾向)
普通
(1000〜1015hPa)
晴れ
(回復傾向)
曇り
(継続)

(悪化傾向)
低気圧
(1000hPa未満)
曇り
(回復傾向)

(継続)

(継続)
STEP
天気予測表示

ステップ3で判断した天気傾向に応じて、LEDを制御します。

2色LEDで表現できる色は3色ですので、曇りの2つの状態(悪化傾向と回復傾向)は区別せず、次のように天気予測表示をしたいと思います。

天気傾向LED点灯色
晴れ
曇り(天気回復傾向)
(赤と青を同時点灯)
曇り(天気悪化傾向)

動作仕様

上のアルゴリズムを実装する上で、プログラムの動作仕様をいくつか決めておく必要があります。

あとから動作を変更しやすいように、動作パラメータはなるべくプログラム先頭で名前定義するようにしたいと思います。

天気予測のための海面気圧の計算

天気予測は海面気圧を使用します。(高気圧、低気圧の判定を行うためです)

BME280で測定する気圧データは現地の気圧データで、海面気圧とは差があります。

そこで、次の一般的な海面気圧算出式を使用して測定した気圧データから海面気圧を計算するようにしました。(べき乗計算が必要ですので、プログラムではmath.hをインクルードしています。プログラムサイズが大きくなりそうですね…)

測定地点の標高はあらかじめ調べておき、プログラムで#defineします。

また気温はADT7410で測定したデータを使用します。

天気予測のための気圧測定間隔

気圧測定は1分ごとに行い、測定結果はLCDモジュールに表示します。

また、測定した気圧データは測定誤差や一時的な環境による影響があります。(ドアの開け閉めや換気扇のON/OFFで変動することがあります)

そのため、天気予測のための気圧データは移動平均をとった値とします。

過去何個分の移動平均を取るかはプログラムで名前定義することにします。

必要データが揃うまでの時間

天気予測は、3時間前の気圧との差分により気圧変化の傾向を調べます。

電源投入後、3時間(プラス移動平均算出に必要な時間)はデータが揃わないため天気予測ができません。

そこで、必要データが揃っていない期間は、赤のLEDを5秒に1回、短くピカッピカッと2回点灯することにしました。

天気予測結果のLED点灯制御

LEDは点灯したままですと消費電力が大きくなりますので、3秒ごとに50ms点灯するようにしてみます。

プログラム構造概要

今まで作成したプログラムに、天気予報のプログラムを追加します。

先ほど検討したアルゴリズムをwhile文の中に実装していきます。

すでに温湿度・気圧測定とLCDモジュールへの表示の部分はありますので、これに加えて天気予報の部分とLED表示の部分を追加します。

while文は1分に1回まわるようにして、LEDの点滅部分で1分間の時間稼ぎをすることにしてみました。

プログラムのwhile文の構造は以下のようになります。

温湿度と気圧データのLCDへの表示は1分に1回、天気予報は5分に1回になりますが、それぞれいろいろな処理をしているので、正確に1分に1回、5分に1回、というわけではない点に注意してください。

プログラム

変更可能なパラメータ

天気予測に必要な値は名前定義として変更できるようにしてみました。

それぞれ次の名前で定義しています。

名前内容
ALTITUDE測定地点の標高で単位はmです。なるべく正確な値にすると良いと思います。測定地点の標高の調べ方は第35回の記事で解説しています。
MEASUREMENT_INTERVAL_MINUTES測定間隔で単位は分です。
TREND_CALCULATION_HOURS気圧変化の速度を、何時間前の気圧から計算するかです。単位は時間です。
MOVING_AVERAGE_SAMPLES移動平均を取るときのサンプル数です。
PRESSURE_TREND_THRESHOLD気圧変化が速いと判断する気圧差です。単位はhPaです。
PRESSURE_BAND_HIGHこれより高い気圧のとき、高気圧帯にいる、と判断します。単位はhPaです。
PRESSURE_BAND_LOWこれより低い気圧のとき、低気圧帯にいる、と判断します。単位はhPaです。

プログラム

/*
 * 天気予報機能付き温湿度・気圧計
 */


// --- 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)

#include <xc.h>
#include <stdint.h>  // int_t型を使用
#include <stdbool.h> // bool型を使用
#include <stdio.h>   // printfのフォーマットで使用
#include <math.h>    // 海面気圧に変換するためにべき乗計算を使用


// --- I2C Ack/Nack定義 ---
#define I2C_ACK  0x00
#define I2C_NACK 0xff

// --- LCDモジュール ---
#define LCD_I2C_ADDRESS 0x7c  // LCDモジュールのI2Cアドレス

// --- ADT7410温度センサ ---
#define ADT7410_I2C_WRITE_ADDRESS 0x90  // ADT7410のI2Cアドレス(Write)
#define ADT7410_I2C_READ_ADDRESS  0x91  // ADT7410のI2Cアドレス(Read)
#define ADT7410_CONFIG_ADDRESS    0x03  // コンフィグレーション設定アドレス
#define ADT7410_TEMP_ADDRESS      0x00  // 温度格納アドレス(上位8ビット)
#define ADT7410_CONFIG_VALUE      0x80  // 設定値(16-bit分解能指定、他はデフォルト)

// --- SPIピン設定 ---
#define SPI_SCK    LATCbits.LATC7
#define SPI_MISO   PORTCbits.RC6
#define SPI_MOSI   LATCbits.LATC5
#define SPI_CSB    LATCbits.LATC4

// --- LEDピン設定 ---
#define LED_RED   LATBbits.LATB0  // 晴れの予報
#define LED_BLUE  LATBbits.LATB1  // 雨の予報
// 曇りは両方点灯して紫色にする


// --- LCDモジュール表示制御関数 ---
void lcdInitialize(void);               // LCD初期化
void lcdClearDisplay(void);             // ディスプレイ全消去
void lcdSendCommandData(uint8_t);       // コマンド送信
void lcdSendCharacterData(uint8_t);     // 1文字表示
void lcdSendString(char *);             // 文字列表示
void lcdLocateCursor(uint8_t,uint8_t);  // カーソル位置指定

// --- LCDモジュールI2Cプロトコル関数 ---
void lcdI2CProtocol(uint8_t, uint8_t, uint8_t);

// --- ADT7410温度センサ制御関数 ---
void  adt7410Config(uint8_t);       // 動作設定
float adt7410GetTemperature(void);  // 温度取得

// --- I2Cプロトコル各信号の生成関数 ---
void    i2cProtocolStart(void);        // スタートコンディション生成
void    i2cProtocolRepeatStart(void);  // リピートスタートコンディション生成
void    i2cProtocolStop(void);         // ストップコンディション生成
void    i2cProtocolSendData(uint8_t);  // 1バイトデータ送信
uint8_t i2cProtocolReceiveData(void);  // バイトデータ受信
uint8_t i2cProtocolCheckAck(void);     // ACK/NACK信号チェック
void    i2cProtocolSendAck(void);      // ACK送信
void    i2cProtocolSendNack(void);     // NACK送信

// --- BME280センサー制御 ---
void     bme280Initialization(void);            // BME280の初期化
void     bme280ForcedMeasurement(void);         // 温湿度・気圧データの測定指示
void     bme280ReadTrimmingParameters(void);    // 補正データの読み取り
void     bme280ReadMeasuredRawData(void);       // 補正前の温湿度・気圧データ読み取り
int32_t  bme280CompensateTemperature(void);     // 温度データ補正
uint32_t bme280CompensatePressure(void);        // 気圧データ補正
uint32_t bme280CompensateHumidity(void);        // 湿度データ補正

// --- SPI通信制御関数 ---
uint16_t spiRead2BytesData(uint8_t address);
uint8_t  spiRead1ByteData(uint8_t address);
void     spiWrite1ByteData(uint8_t address, uint8_t data);
void     spiSend8bit(uint8_t data);
uint8_t  spiReceive8bit(void);


// --- 動作周波数 (__delay_ms()関数の時間基準) ---
#define _XTAL_FREQ 4000000


// --- BME280温湿度・気圧読み取りデータ ---
uint32_t hum_raw, temp_raw, pres_raw;
int32_t  t_fine;

// --- BME280気温補正データ ---
uint16_t dig_T1;
int16_t  dig_T2;
int16_t  dig_T3;

// --- BME280湿度補正データ ---
uint8_t  dig_H1;
int16_t  dig_H2;
uint8_t  dig_H3;
int16_t  dig_H4;
int16_t  dig_H5;
int8_t   dig_H6;

// --- BME280気圧補正データ ---
uint16_t dig_P1;
int16_t  dig_P2;
int16_t  dig_P3;
int16_t  dig_P4;
int16_t  dig_P5;
int16_t  dig_P6;
int16_t  dig_P7;
int16_t  dig_P8;
int16_t  dig_P9;


// --- 測定地点の標高(m) できればメートル単位で正確に ---
#define ALTITUDE (50.0f)

// --- 天気予報用 ---
#define MEASUREMENT_INTERVAL_MINUTES  1  // 測定間隔(分) 注意:1時間の測定回数が整数になるように設定する
#define TREND_CALCULATION_HOURS       3  // 気圧変化の速度を見るために、何時間前の気圧からの変化を見るか(時間)
#define MOVING_AVERAGE_SAMPLES       10  // 移動平均のサンプル数


// 気圧変化のしきい値(hPa)
#define PRESSURE_TREND_THRESHOLD     (1.6f)

// 気圧バンドのしきい値
#define PRESSURE_BAND_HIGH           (1015.0f)
#define PRESSURE_BAND_LOW            (1000.0f)

// 保持すべきデータ数を計算
#define SAMPLES_PER_HOUR (60 / MEASUREMENT_INTERVAL_MINUTES)
#define HISTORY_SIZE     (SAMPLES_PER_HOUR * TREND_CALCULATION_HOURS)

// --- 移動平均用グローバル変数 ---
// 生の気圧データを一時的に保存するバッファ
float raw_pressure_buffer[MOVING_AVERAGE_SAMPLES];
// 生データ用バッファの現在位置を示すインデックス
int raw_buffer_index = 0;
// 生データ用バッファのデータ蓄積数カウンタ
int raw_data_count = 0;

// 気圧履歴を保存するリングバッファ配列
float pressure_history[HISTORY_SIZE];
// リングバッファの現在位置を示すインデックス
int history_index = 0;
// 履歴データが十分に溜まったかを判断するためのカウンタ
int history_data_count = 0;

// 天気予報の結果を格納する列挙型
enum Weather {SUNNY, CLOUDY, RAINY};


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

    // 動作周波数設定
    OSCCON1bits.NDIV = 0b0000;  // 分周1:1
    OSCFRQbits.HFFRQ = 0b010;   // 4MHz
    
    // ピン属性設定
    ANSELA = 0b00000000;
    ANSELB = 0b00000000;
    ANSELC = 0b00000000;
    TRISA  = 0b00000000;
    TRISB  = 0b00000000;
    TRISC  = 0b01001100;
    
    // LED消灯
    LED_RED  = 0;
    LED_BLUE = 0;
    
    // SPI信号線初期設定
    SPI_SCK  = 0;  // クロックを0
    SPI_MOSI = 0;  // ホストからデバイスを0
    SPI_CSB  = 1;  // チップセレクトを1(=無効)

    // --- I2C通信ピンのPPS設定 ---
    // 設定ロック解除
    PPSLOCK = 0x55;
    PPSLOCK = 0xAA;
    PPSLOCKbits.PPSLOCKED = 0x00;

    // SCL, SDAピンの割り当て
    SSP1DATPPS = 0x12; // RC2をMSSP1:SDA1に設定
    SSP1CLKPPS = 0x13; // RC3をMSSP1:SCL1に設定
    RC3PPS = 0x14;     // RC3をMSSP1:SCL1に設定
    RC2PPS = 0x15;     // RC2をMSSP1:SDA1に設定

    // 設定ロック
    PPSLOCK = 0x55;
    PPSLOCK = 0xAA;
    PPSLOCKbits.PPSLOCKED = 0x01;
    
    // --- I2C通信設定 ---
    // SMP Standard Speed; CKE disabled; 
    SSP1STAT = 0x80;
    // SSPEN enabled; CKP Idle:Low, Active:High; SSPM FOSC/4_SSPxADD_I2C; 
    SSP1CON1 = 0x28;
    // SBCDE disabled; BOEN disabled; SCIE disabled; PCIE disabled; DHEN disabled; SDAHT 300ns; AHEN disabled; 
    SSP1CON3 = 0x00;
    // Baud Rate Generator = 100kHz 
    SSP1ADD = 0x09;
    
    // LCDモジュール電源安定化時間待ち
    __delay_ms(100);
    
    // LCD初期化
    lcdInitialize();
    
    // LCD表示クリア
    lcdClearDisplay();
    
    // ADT7410温度センサ初期化
    adt7410Config(ADT7410_CONFIG_VALUE);

    // BME280初期化 (コントロールレジスタ、補正係数取得)
    bme280Initialization();

    // 実際の温湿度・気圧
    float actual_temp;   // ADT7410測定値
    float actual_hum;    // BME280測定値
    float actual_press;  // BME280測定値

    // BME280補正データ
    int32_t  compensated_temp;
    uint32_t compensated_hum;
    uint32_t compensated_press;
    
    // 気圧測定回数計測
    uint8_t pressure_counter = 0;
    
    // 気圧変化測定用
    uint32_t last_pressure = 0;
    int16_t pressure_diff;
    
    // 天気予報
    enum Weather weather_forecast = SUNNY;
    
    // LED表示用ループカウンタ
    uint8_t led_counter;

    // ----- 予測できるデータが揃っているかフラグ -----
    bool possible_forecast = false;
    
    // 測定・データ表示
    while(1) {

        LED_BLUE = 1;
        __delay_ms(1000);
        LED_BLUE = 0;
        __delay_ms(1000);
        
        // --- 天気予報を格納する変数 ---
        enum Weather forecast;
        
        /// ----- 気温の測定とLCD表示 -----
        float adt7410_temp = adt7410GetTemperature();
        lcdLocateCursor(0, 0);
        printf("%5.2f", adt7410_temp);
        lcdSendCharacterData(0xdf);
        lcdSendCharacterData('C');


        // ----- BME280から温湿度・気圧データを取得 -----
        // 測定指示
        bme280ForcedMeasurement();
        
        // 本来、次のようにステータスレジスタ(アドレス:0xf3)が0になるまで待った方がよいが、単に時間待ちをする
        // (0以外の場合は、測定中かデータコピー中。0の場合は読み出し可能)
        //while(spiRead1ByteData(0xF3) & 0x04) {
        //    __delay_ms(10);
        //}
        __delay_ms(1);
        
        // 測定データ読み出し
        bme280ReadMeasuredRawData();

        // 取得したデータをキャリブレーション
        compensated_temp  = bme280CompensateTemperature();
        compensated_hum   = bme280CompensateHumidity();
        compensated_press = bme280CompensatePressure();


        // ----- 湿度のLCD表示 -----
        actual_hum = (float)compensated_hum / 1024.0;
        printf("  %3.0f%%", actual_hum);  // 気温の後にスペースを入れて表示


        // ----- 気圧のLCD表示 -----
        // 実際の気圧データに変換
        actual_press = (float)compensated_press / 100.0;
        // 気圧を海面気圧に変換
        float sea_level_press = actual_press * pow(1 - 0.0065*ALTITUDE/(adt7410_temp+0.0065*ALTITUDE+273.15), -5.257);
        // 海面気圧を表示
        lcdLocateCursor(0, 1);
        printf("%7.2fhPa", sea_level_press);
        
        //
        // ----- 気圧データから天気を予測 -----
        //
        
        // ----- 移動平均計算用バッファに生の気圧データを格納 -----
        raw_pressure_buffer[raw_buffer_index] = sea_level_press;

        // ----- 次に生の気圧データを保存する位置へインデックスを移動 -----
        raw_buffer_index++;
        if (raw_buffer_index >= MOVING_AVERAGE_SAMPLES) {
            raw_buffer_index = 0;
        }

        // ----- 移動平均データ数のカウント(最大値はMOVING_AVERAGE_SAMPLES)
        if (raw_data_count < MOVING_AVERAGE_SAMPLES) {
            raw_data_count++;
        }

        // 移動平均のサンプル数が溜まってから処理をする
        if (raw_data_count >= MOVING_AVERAGE_SAMPLES) {
            
            // ----- 気圧の移動平均を計算 -----
            float pressure_sum = 0.0f;
            for (int i=0; i<MOVING_AVERAGE_SAMPLES; i++) {
                pressure_sum += raw_pressure_buffer[i];
            }
            float moving_avg_press = pressure_sum / MOVING_AVERAGE_SAMPLES;
            
            // ----- 移動平均値を履歴配列(リングバッファ)に保存
            pressure_history[history_index] = moving_avg_press;

            // ----- 次に保存する位置へインデックスを移動 -----
            // 移動したインデックス値には、過去の比較する気圧データが入っている(TREND_CALCULATION_HOURS時間前の気圧データ)
            history_index++;
            if (history_index >= HISTORY_SIZE) {
                history_index = 0;
            }
            
            // ----- 履歴データ数のカウント(最大値はHISTORY_SIZE) -----
            if (history_data_count < HISTORY_SIZE) {
                history_data_count++;
            }

            // ----- 天気予測に必要なデータが揃ってたら処理をする
            if (history_data_count >= HISTORY_SIZE) {
                
                // 予測可能になったので予測フラグをON
                possible_forecast = true;
                
                // --- 過去の比較気圧データを取得 ---
                // 現在の気圧データは moving_avg_pressure または pressure_history[history_index-1] に入っている
                // 比較気圧データはpressure_history[history_index] に入っている(367〜369行でオーバーフロー対応済み)
                float pressure_hours_ago = pressure_history[history_index];

                // ----- 気圧の変化量を計算 -----
                float pressure_trend = moving_avg_press - pressure_hours_ago;
                
                // ----- 天気予測の判定条件に従って天気を予測
                // 高気圧帯の場合
                if (moving_avg_press >= PRESSURE_BAND_HIGH) {
                    if (pressure_trend < -PRESSURE_TREND_THRESHOLD) {
                        forecast = CLOUDY; // 下降傾向なら「曇り」
                    } else {
                        forecast = SUNNY;  // それ以外(変化なし・上昇)は「晴れ」
                    }
                }
                // 普通の気圧の場合
                else if (moving_avg_press >= PRESSURE_BAND_LOW) {
                    if (pressure_trend > PRESSURE_TREND_THRESHOLD) {
                        forecast = SUNNY;  // 上昇傾向なら「晴れ」
                    } else if (pressure_trend < -PRESSURE_TREND_THRESHOLD) {
                        forecast = RAINY;  // 下降傾向なら「雨」
                    } else {
                        forecast = CLOUDY; // 変化なしなら「曇り」
                    }
                }
                // 低気圧帯の場合
                else {
                    if (pressure_trend > PRESSURE_TREND_THRESHOLD) {
                        forecast = CLOUDY; // 上昇傾向なら「曇り」
                    } else {
                        forecast = RAINY;  // それ以外(変化なし・下降)は「雨」
                    }
                }
            }
        }

        // ----- LED点灯制御 ----
        // *** ここで測定間隔分の時間待ちをする ***
        if(possible_forecast) {
            // 予測ができていれば3秒に1回短く点灯
            #define REPEAT_NUMBER_FORECAST (MEASUREMENT_INTERVAL_MINUTES*60/3) 
            // 晴れ
            if(forecast == SUNNY) {
                for(int i=0; i<REPEAT_NUMBER_FORECAST; i++) {
                    LED_BLUE = 0;
                    LED_RED  = 1;
                    __delay_ms(50);
                    LED_RED  = 0;
                    __delay_ms(2950);
                }
            }
            // 曇り
            if(forecast == CLOUDY) {
                for(int i=0; i<REPEAT_NUMBER_FORECAST; i++) {
                    LED_BLUE = 1;
                    LED_RED  = 1;
                    __delay_ms(50);
                    LED_BLUE = 0;
                    LED_RED  = 0;
                    __delay_ms(2950);
                }
            }
            // 雨
            if(forecast == RAINY) {
                for(int i=0; i<REPEAT_NUMBER_FORECAST; i++) {
                    LED_BLUE = 1;
                    LED_RED  = 0;
                    __delay_ms(50);
                    LED_BLUE = 0;
                    __delay_ms(2950);
                }
            }
        } else {
            // 予測データが足りない場合は5秒に1回、赤色LEDを短く2回点灯
            for(int i=0; i<MEASUREMENT_INTERVAL_MINUTES*60/5; i++) {
                LED_RED = 1;
                __delay_ms(50);
                LED_RED = 0;
                __delay_ms(150);
                LED_RED = 1;
                __delay_ms(50);
                LED_RED = 0;
                __delay_ms(4750);
            }
        }



    }

}


//
// ADT7410温度センサ関数
//

// ADT7410動作設定
void adt7410Config(uint8_t config_value) {
    
    i2cProtocolStart();                              // スタートコンディション生成
    i2cProtocolSendData(ADT7410_I2C_WRITE_ADDRESS);  // デバイスアドレス送信
    i2cProtocolSendData(ADT7410_CONFIG_ADDRESS);     // 動作設定アドレス送信
    i2cProtocolSendData(config_value);               // 動作設定値送信
    i2cProtocolStop();                               // ストップコンディション生成
    
    return;
}

// ADT7410温度取得
float adt7410GetTemperature(void) {
    
    uint8_t temp_high, temp_low;
    int16_t temp_value;
    
    i2cProtocolStart();                              // スタートコンディション生成
    i2cProtocolSendData(ADT7410_I2C_WRITE_ADDRESS);  // デバイスアドレス送信(書き込み指定)
    i2cProtocolSendData(ADT7410_TEMP_ADDRESS);       // 温度データ読み取りのアドレス送信
    i2cProtocolRepeatStart();                        // リピートスタートコンディション生成
    i2cProtocolSendData(ADT7410_I2C_READ_ADDRESS);   // デバイスアドレス送信(読み取り指定)
    temp_high = i2cProtocolReceiveData();            // 1バイトデータ受信
    i2cProtocolSendAck();                            // ACK送信
    temp_low  = i2cProtocolReceiveData();            // 1バイトデータ受信
    i2cProtocolSendNack();                           // NACK送信
    i2cProtocolStop();                               // ストップコンディション生成
    
    // 温度計算
    if( temp_high & 0x80 ) {
        temp_value = ( (temp_high & 0x7f) << 8 ) + temp_low - 32768;
    } else {
        temp_value = ( (temp_high & 0x7f) << 8 ) + temp_low;
    }

    return (float)temp_value / 128.0;
}



//
// LCD制御関数
//

//
// printfがコールする1文字出力関数の定義
//
void putch(char character) {

    lcdSendCharacterData(character);
    
    return;
}

//
// LCDモジュールに制御コードまたはデータを送信
//
void lcdI2CProtocol(uint8_t address, uint8_t control_code, uint8_t data) {
    
    i2cProtocolStart();                 // スタートコンディション
    i2cProtocolSendData(address);       // アドレス送信
    i2cProtocolSendData(control_code);  // 制御コード送信 (動作設定=0x00/文字表示=0x40)
    i2cProtocolSendData(data);          // データ送信
    i2cProtocolStop();                  // ストップコンディション

    return;
}

//
// 表示文字データ送信
//   0x40の後にデータを送信
void lcdSendCharacterData(uint8_t data){

    // 表示文字のデータを送信する場合の制御コードは0x40
    lcdI2CProtocol(LCD_I2C_ADDRESS, 0x40, data);
    
    // ウエイト
    //   文字表示の場合はウエイトは必要なくても動作しているが
    //   表示されない場合は1ms程度のウエイトを入れる
    // __delay_ms(1);

    return;
}

//
// コマンド送信
//   0x00の後にコマンドを送信
//
void lcdSendCommandData(uint8_t command){

    // コマンドを送信する場合の制御コードは0x00
    lcdI2CProtocol(LCD_I2C_ADDRESS, 0x00, command);

    // ウエイト
    //   データシートではウエイト時間は26.3us以上になっているが、
    //   それより長くしないと初期化できないケースがあるため1msのウエイトを入れる
    __delay_ms(1);
    
    return;
}

//
// ディスプレイ消去
//
void lcdClearDisplay(void){
    
    lcdSendCommandData(0x01);
    
    return;
}

//
// カーソル位置移動
//    引数は水平方向右側プラスのX軸、垂直方向下側プラスのY軸で、それぞれ0から開始
//    左上の座標が(x=0, y=0)
//
void lcdLocateCursor(uint8_t position_x, uint8_t position_y){
    
    // 文字表示位置指定コマンド送信
    lcdSendCommandData( 0x80 + 0x40 * position_y + position_x );
    
    return;
}

//
// 文字列を送信
//
void lcdSendString(char *str){
    
    // strの文字列を*strが0になるまでLCDモジュールに送信
    while(*str) {
        lcdSendCharacterData(*str);
        str++;
    }
    
    return;
}


//
// LCDモジュール初期化
//
void lcdInitialize(void){

    // 初期化コマンド送信
    lcdSendCommandData(0x38); // 2行モードに設定
    lcdSendCommandData(0x39); // 拡張コマンド選択
    lcdSendCommandData(0x14); // 内部クロック周波数設定
    lcdSendCommandData(0x70); // コントラスト設定(C3:C0 = 0b0000に設定)
    lcdSendCommandData(0x56); // 電源電圧が3.3VなのでBooster=ON、コントラスト設定(C5:C4 = 0b10に設定)
    lcdSendCommandData(0x6c); // オペアンプのゲイン設定
    
    // モジュール内電源安定化のための時間待ち
    __delay_ms(200);
    
    // 初期化コマンド続き
    lcdSendCommandData(0x38); // 通常コマンド選択
    lcdSendCommandData(0x01); // ディスプレイ表示内容クリア
    lcdSendCommandData(0x0c); // ディスプレイ表示
    
    return;
}


//
// I2Cプロトコル制御関数
//

// スタートコンディション生成
void i2cProtocolStart() {
    
    // SSP1CON2レジスタのSENビットを1に設定すると
    // スタートコンディションが生成される
    // 発行が完了するとSSP1IFが1になるのでwhile文で待つ
    SSP1IF = 0;
	SSP1CON2bits.SEN = 1;
	while (SSP1IF == 0) {}
    SSP1IF = 0;
    
	return;
}

// リピートスタートコンディション生成
void i2cProtocolRepeatStart() {
    
	SSP1IF = 0;
	SSP1CON2bits.RSEN = 1;
	while (SSP1IF == 0) {}
    SSP1IF = 0;

	return;
}

// ストップコンディション生成
void i2cProtocolStop() {

    // SSP1CON2レジスタのPENビットを1に設定すると
    // ストップコンディションが生成される
    // 発行が完了するとSSP1IFが1になるのでwhile文で待つ
	SSP1IF = 0;
	SSP1CON2bits.PEN = 1;
	while (SSP1IF == 0) {}
	SSP1IF = 0;

	return;
}

// 1バイトデータ送信
void i2cProtocolSendData(uint8_t data) {

    // SSP1BUFに送信したいデータをセットすると、そのデータが送信される
    // 発行が完了するとSSP1IFが1になるのでwhile文で待つ
	SSP1IF = 0;
	SSP1BUF = data;
	while (SSP1IF == 0) {}
    SSP1IF = 0;
    
	return;
}

// 1バイトデータ受信
uint8_t i2cProtocolReceiveData() {
    
	SSP1IF = 0;
	SSP1CON2bits.RCEN = 1;
	while (SSP1IF == 0) {}
    SSP1IF = 0;

	return SSP1BUF;
}

// Ack/Nackチェック
uint8_t i2cProtocolCheckAck() {
    
	uint8_t ackStatus;

	if (SSP1CON2bits.ACKSTAT) {
		ackStatus = I2C_NACK;
	} else {
		ackStatus = I2C_ACK;
	}

	return ackStatus;
}

// Ack送信
void i2cProtocolSendAck() {
    
    // ACKDTにACKをセット(負論理なので0を設定)
	SSP1CON2bits.ACKDT = 0;

    // NACK信号生成
	SSP1CON2bits.ACKEN = 1;
	while (SSP1CON2bits.ACKEN) {}

	return;
}

// Nack送信
void i2cProtocolSendNack() {
    
    // ACKDTにNACKをセット(負論理なので1を設定)
	SSP1CON2bits.ACKDT = 1;

    // NACK信号生成
	SSP1CON2bits.ACKEN = 1;
	while (SSP1CON2bits.ACKEN) {}

	return;
}



//
// BME280初期化関数
//
void bme280Initialization(void) {

    // 動作パラメータ設定
    uint8_t t_sb     = 0;  // スタンドバイ時間は使用しない
    uint8_t filter   = 0;  // フィルタOFF
    uint8_t spi3w_en = 0;  // SPIは4線式(=0)
    uint8_t osrs_t   = 1;  // 温度オーバーサンプリング x1
    uint8_t osrs_p   = 1;  // 大気圧オーバーサンプリング x1
    uint8_t osrs_h   = 1;  // 湿度オーバーサンプリング x1
    uint8_t mode     = 0;  // スリープモード

    // 設定値をフォーマットに合わせる
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;

    // BME280動作パラメータ書き込み
    spiWrite1ByteData(0xF2, ctrl_hum_reg);
    spiWrite1ByteData(0xF4, ctrl_meas_reg);
    spiWrite1ByteData(0xF5, config_reg);
    
    // センサ処理待ち
    __delay_ms(1000);

    // 補正値読み込み
    //   センサごとに固定ちのため、初期化時のみ読み込む
    bme280ReadTrimmingParameters();

}


//
// 温湿度・気圧データ測定指示
//
void bme280ForcedMeasurement(void) {

    // 動作パラメータ設定
    uint8_t t_sb     = 0;  // スタンドバイ時間は使用しない
    uint8_t filter   = 0;  // フィルタOFF
    uint8_t spi3w_en = 0;  // SPIは4線式
    uint8_t osrs_t   = 1;  // 温度オーバーサンプリング x1
    uint8_t osrs_p   = 1;  // 大気圧オーバーサンプリング x1
    uint8_t osrs_h   = 1;  // 湿度オーバーサンプリング x1
    uint8_t mode     = 1;  // 測定指示(1回測定したらスリープモードに移行)

    // 設定値をフォーマットに合わせる
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;

    // BME280動作パラメータ書き込み
    spiWrite1ByteData(0xF2, ctrl_hum_reg);
    spiWrite1ByteData(0xF4, ctrl_meas_reg);
    spiWrite1ByteData(0xF5, config_reg);
    
    // 測定待ち
    //
    __delay_ms(10);
    
    // この段階でBME280の温湿度大気圧レジスタに測定値が格納されている
    // このあとスリープモードに入が、温湿度・気圧データの読み取りは可能

}


//
// 補正データ読み込み
//
void bme280ReadTrimmingParameters(void) {

    // 気温データ用補正データ
    dig_T1 = spiRead2BytesData(0x88);
    dig_T2 = (int16_t)spiRead2BytesData(0x8A);
    dig_T3 = (int16_t)spiRead2BytesData(0x8C);

    // 気圧データ用補正データ用補正データ
    dig_P1 = spiRead2BytesData(0x8E);
    dig_P2 = (int16_t)spiRead2BytesData(0x90);
    dig_P3 = (int16_t)spiRead2BytesData(0x92);
    dig_P4 = (int16_t)spiRead2BytesData(0x94);
    dig_P5 = (int16_t)spiRead2BytesData(0x96);
    dig_P6 = (int16_t)spiRead2BytesData(0x98);
    dig_P7 = (int16_t)spiRead2BytesData(0x9A);
    dig_P8 = (int16_t)spiRead2BytesData(0x9C);
    dig_P9 = (int16_t)spiRead2BytesData(0x9E);

    // 湿度データ用補正データ用補正データ
    dig_H1 = spiRead1ByteData(0xA1);
    dig_H2 = (int16_t)spiRead2BytesData(0xE1);
    dig_H3 = spiRead1ByteData(0xE3);
    dig_H4 = (int16_t)((spiRead1ByteData(0xE4) << 4) | (spiRead1ByteData(0xE5) & 0x0F));
    dig_H5 = (int16_t)((spiRead1ByteData(0xE6) << 4) | (spiRead1ByteData(0xE5) >> 4));
    dig_H6 = (int8_t)spiRead1ByteData(0xE7);

}


// 
// 補正前の生の温湿度・気圧データ読み込み
// 
void bme280ReadMeasuredRawData() {

    // データ読み取り用配列
    uint32_t data[8];

    // デバイスセレクトアクティブ
    SPI_CSB = 0;

    // 読み込み開始アドレス指定
    spiSend8bit(0xF7 | 0b10000000);

    // 8バイト分のデータ読み込み
    for(int8_t i=0; i<8; i++){
      data[i] = spiReceive8bit();
    }

    // デバイスセレクトインアクティブ
    SPI_CSB = 0;

    // 読み込みしたデータから気温、湿度、気圧データを生成
    pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
    temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
    hum_raw  = (data[6] <<  8) | data[7];

}

//
// 気温データ補正
//   何をしているのかよくわからない
// 
int32_t bme280CompensateTemperature() {
  
    int32_t var1, var2, T;

    var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;

    return T; 

}


//
// 気圧データ補正
//   何をしているのかよくわからない
// 
uint32_t bme280CompensatePressure() {

    int32_t var1, var2;
    uint32_t P;

    var1 = (((int32_t)t_fine)>>1) - (int32_t)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11) * ((int32_t)dig_P6);
    var2 = var2 + ((var1*((int32_t)dig_P5))<<1);
    var2 = (var2>>2)+(((int32_t)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((int32_t)dig_P2) * var1)>>1))>>18;
    var1 = ((((32768+var1))*((int32_t)dig_P1))>>15);
    if (var1 == 0)
      return 0;

    P = (((uint32_t)(((int32_t)1048576)-pres_raw)-(var2>>12)))*3125;
    if(P<0x80000000)
      P = (P << 1) / ((uint32_t) var1);   
    else
      P = (P / (uint32_t)var1) * 2;

    var1 = (((int32_t)dig_P9) * ((int32_t)(((P>>3) * (P>>3))>>13)))>>12;
    var2 = (((int32_t)(P>>2)) * ((int32_t)dig_P8))>>13;
    P = (uint32_t)((int32_t)P + ((var1 + var2 + dig_P7) >> 4));

    return P;

}

//
// 湿度データ補正
//   何をしているのかよくわからない
// 
uint32_t bme280CompensateHumidity() {

    int32_t v_x1_u32r;

    v_x1_u32r = (t_fine - ((int32_t)76800));
    v_x1_u32r = (((((hum_raw << 14) -(((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1_u32r)) + 
             ((int32_t)16384)) >> 15) * (((((((v_x1_u32r * ((int32_t)dig_H6)) >> 10) * 
             (((v_x1_u32r * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t)2097152)) * 
             ((int32_t) dig_H2) + 8192) >> 14));
    v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
    // v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r);
    if( v_x1_u32r < 0 ){
        v_x1_u32r = 0;
    }
    // v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r);
    if( v_x1_u32r > 419430400 ) {
        v_x1_u32r = 419430400;
    }

    return (uint32_t)(v_x1_u32r >> 12);

}


//
// SPIデータを指定アドレスから2バイト読み込み
// 
uint16_t spiRead2BytesData(uint8_t address) {

    uint8_t  data_low, data_high;  // 読み込んだ1バイトデータ格納用
    uint16_t data;  // 2バイトデータ用

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

    // 読み込みデータアドレス指定
    spiSend8bit(address);

    // 指定アドレスとその次のアドレスのデータを2バイト読み込み
    data_low  = spiReceive8bit();
    data_high = spiReceive8bit();

    //16ビットデータにする
    data = (data_high << 8) | data_low;

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

    return data;

}


//
// SPIデータを指定アドレスから1バイト読み込み
// 
uint8_t spiRead1ByteData(uint8_t address) {

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

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

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

    // SPI通信手順によりデータを受信
    data = spiReceive8bit();

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

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

}


//
// SPIデータを指定アドレスに1バイト書き込み
//
void spiWrite1ByteData(uint8_t address, uint8_t data) {

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

    // アドレス指定(書き込みは最上位ビット0)
    spiSend8bit(address & 0b01111111);

    // データ書き込み
    spiSend8bit(data);

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

//
// 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.10.18新規投稿
2019.5.14BME280の強制測定モードの際、測定が完了するまで待つ処理(10ms)を追加
2025.8.29新しいアルゴリズムでプログラム実装
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
junjun
junjun
5 年 前

PICの勉強でお世話になってます。

温度/湿度/気圧計を作成し、リビングで活躍しています。
作成後2ヶ月、表示に不具合が発生しました。
不具合は気圧の横に、温度が表示されるのです。
電源を入れ直すと元に戻るのですが、2度目の不具合が発生しました。
プログラムを見直したのですが、問題はないと思います。(コピペですが^^;)
回路は、USBからレギョレータ(3.3V)の電源を使用しています。
何か、情報がありましたら共有をお願いします。

comment image comment image

管理者
管理者
返信  junjun
5 年 前

コメントどうもありがとうございました。またお返事が遅くなってしまい申し訳ございませんでした。

私もリビングで運用していますが、特にこのような現象は見られませんでしたが、実際に発生していますので何かしら不具合原因があるのだと思います。

気圧のすぐ後に気温が表示されている、ということは、気温の表示位置が(0, 0)になっていない、ということですので、このあたりの対策が必要なのだと思います。

すみません、即答はできませんが原因と対策を考えてみます。何か分かりましたらこのコメントに返信いたします。

ところで、カバーををつけられたんですね。画像をみたときに何かの製品かと思いましたが、よくよく見るとLCDディスプレイの枠がありましので、カバーをつけられたんだな、と思いました。自作のものはなんだか愛着が出てきますよね。さらに中身もわかっていて、カスタマイズも自由ですので、市販製品にはない楽しみがあると思います。ぜひ天気予報のアルゴリズムも改良するなど楽しんでいただければと思います。

目次