以下の内容はhttps://negligible.hatenablog.com/entry/2025/10/13/020750より取得しました。


気圧計を作る ~ PICマイコン編 ~

はじめに

前記事ではM5Stackに気圧センサLPS25Hを接続し,計測した温度と気圧をmicroSDカードに保存する気圧ロガーを作りました(今はM5Stack Protoモジュール13.2に収めようと画策しています)。本記事ではPICマイコンを用いたボタン電池駆動の「かわいい」気圧計を作ります。

PICマイコンと私

筆者がPICマイコンと出会ったのは2005年頃だったのではないかと思い出します。トランジスタ技術2005年4月号の付録だったR8C/Tinyマイコン基板で遊んでいた頃,もっと手軽かつ安価なマイコンとしてPICマイコンの存在をどなたからか教えてもらい,秋月電子通商のAKI-PICプログラマーを組み立てました(今でも売っているんですね)。誰もが触った往年の名マイコンPIC16F84AやPIC16F877Aで実験ボードを作ったり,実際に大学での実験に使ったりもしましたね。この時期,開発ツールMPLABで無料で使用できるのはアセンブリ言語だけでした。

しかしその後,PICマイコンはおろか,電子工作をまったくしない時期が10年ほど続きました。筆者の「空白期」「暗黒時代」です。この時期にもっといろいろ学んでいれば…??

それからしばらくして,mbed,M5Stack,Raspberry Piといったマイコンシングルボードコンピュータと出会った筆者は,かつて遊んでいたPICマイコンでもう一度遊んでみたいと思い,MPLAB X IDEをパソコンにインストールし,PICKit 2,PICKit 3(の互換機)をAmazonでポチってみました。今やアセンブリ言語ではなくC言語も無料で使えるではないか…! PIC12F1840でフルカラーLEDを制御するといった練習を試みた後,また2,3年ほど電子工作ができない時期に突入してしまいました。

再び少しずつ工作ができるようになったところで,前記事のM5Stack気圧ロガーを作りましたが,PICマイコンの練習を兼ねて少ピンマイコンでかわいい気圧計を作ろうと思い立ちました。当初は8ピンのPIC12F1840などを用い,microSDカードへの記録も目指していましたが,プログラムメモリが足りないことからmicroSDカードへの記録は諦め,また14ピンのPIC16F18326(プログラムメモリが16 kwordもある!)を使うことにしました。これを秋月電子通商のC基板に収め,かつ,ボタン電池(CR2032)とキャラクLCDと一緒に窓付きブリキ缶ケースに収めると,なかなか見栄えもするのではないか…?

完成イメージ

完成イメージを図1に示します。窓付きブリキ缶ケースの中に秋月電子通商十字配線ユニバーサル基板(Cタイプ)で作成した基板を収めています。基板の中にはバックライト付きキャラクタLCDPIC16F18326温湿度・気圧センサBME280ボタン電池CR2032,タクトスイッチがあります。

図1: PIC16F18326とボタン電池による携帯型気圧計
ボタン電池を入れると直ちに起動し,温度・湿度・気圧を0.5秒毎に表示します。この状態でタクトスイッチを押すとPICマイコンはスリープモードに入り,キャラクLCDの電源も遮断されます。タクトスイッチを再度押すとスリープモードから復帰します。復帰とスリープの様子を動画1に示します。
youtu.be
動画1: 復帰とスリープの様子
ただ単に温度,湿度,気圧を表示するだけで,記録することも,インターネットに報告することも何もできません…。

図2,3に示すように,暗いところで見ると,透明な窓付きブリキ缶ケースの中でキャラクLCDのバックライトと白色LEDが光を放ち,なかなかエモい雰囲気になります。

図2: 白文字黒背景キャラクLCD
図3: 少し暗いところでの様子
白文字黒背景キャラクLCDは僅かながらVFD (vacuum fluorescent display)のような雰囲気がありますね。

以降,このPICマイコン気圧計のハードウェア構成,MPLAB MCCでの設定,そしてソフトウェア構成について述べていきます。

目次

ハードウェア構成

PICマイコン周辺回路

図4に十字配線ユニバーサル基板のカットパターンを示します。14ピンのPICマイコン,PIC16F18326を用いましたが,ブレッドボードでの試作段階ではPIC12F1840,PIC16F19155(図5参照)でも試行錯誤しました*1PICマイコンとBME280,キャラクLCDの接続はいずれもI2Cとなっており,回路は単純です。PICマイコンのI/Oポートにはソフトウェアで設定できるウィークプルアップ機能があるため,外付けのプルアップ抵抗も不要です。PIC16F18326にはI2CまたはSPIのポートとして使用できるMSSP (master synchronous serial port)が2つ搭載されていますが,そのうちのMSSP1をI2Cに設定すると,RC0がSCL,RC1がSDAとなります。

図4: 十字配線ユニバーサル基板のカットパターン
図5: PIC16F19155を用いたブレッドボード試作機
スリープモードとウェイクアップを制御するタクトスイッチもI/OポートRA1に接続しています。ここでもウィークプルアップを設定することで,外付けのプルアップ抵抗を不要としています。また,デバッグや動作確認用に白色LEDをI/OポートRA2に接続しています(電流制限抵抗は2 kΩ)。そして,全体をボタン電池CR2032で給電しています。

省電力化の工夫として,キャラクLCDの本体(内部の制御用ICであるST7032と表示器そのもの)およびバックライト(白色LED)のGND端子をPICマイコンのI/OポートRC2とRC3に並列で接続しています。PICマイコンが通常動作中はRC2とRC3にLowを出力してキャラクLCDに電力を供給し,スリープモードに入る前にRC2とRC3をハイインピーダンスとしてキャラクLCDへの電力を遮断します。このため,RC2とRC3をオープンドレインに設定しています。PIC16F18326のI/Oポートは電流ドライブ能力が高く,1つのピンで何と50 mAまでドライブできます。これを活かして,キャラクLCD全体の電源を入り切りしてしまおうと考えました。なお,温湿度・気圧センサBME280については(BME280自身の)スリープモードでの消費電流がデータシート上のMax値で0.3 μAでしたので,PICマイコンがスリープモードとなっても電源は供給したままとしました。

MPLAB X IDEで書いたプログラムを書き込むため,ICSP (in-circuit serial programming)用のピンヘッダを立てています。MPLAB X IDE v6.25からはPicKit 3がサポートされなくなってしまったため,泣く泣くMPLAB SNAPを購入しました。

ケース実装

某日,千石電商にふらっと立ち寄った際,3階で窓付きブリキ缶ケースを見つけました(ウェブサイトでは発見できませんでした)。Amazonでも同様のケースが売られておりますね*2。このケース,何と秋月電子通商のC基板を収めるのに完璧なサイズなのです。作ろうと思っていた「かわいい」気圧計を作るのに最適と考え,これに実装することとしました。

試作機たち

PIC16F18326,キャラクLCD,BME280,タクトスイッチ,そしてボタン電池CR2032のすべてをC基板に収め,かつ省電力化の工夫も盛り込もうとするとなかなか難儀で,図4のカットパターンにたどり着くまでに試作を繰り返しました。図6に製作した3台,試作零号機,試作初号機,試作弐号機を示します。一番右の「試作弐号機」が最新です(図1 ~ 3のものですが,ここでは窓付きブリキ缶ケースに入れる前です)。落ち着いたら参号機を作ろうと思っています。

図6: 試作零号機,試作初号機,そして試作弐号機
試作零号機はキャラクLCDモジュールのバックライトのみを制御するようにしたため,スリープしていても内部の制御用のICには電源が供給されています。このためか否か,スリープしていても数日で電池切れに至ってしまいます。試作初号機はバックライト無しのキャラクLCDを搭載してみました。やはり暗所で見えないため,バックライトは必要としてバックライト無しは採用見送りとなりました。試作弐号機では上述の工夫を盛り込んでいる他,タクトスイッチのボタンの高さが高い(長い)ものに変更し,ふたを閉めたまま押せるようにしました。

窓付きブリキ缶ケースへの実装

当初は缶ケースに穴をあけてねじ止めしようと考えておりましたが,基板自体が軽いこと(ICソケット類は用いず,PICマイコンLCDモジュールも直接基板にはんだ付けしました),缶ケースの見た目を保ちたかったことから,貼付型スペーサにて缶ケース内に固定することにしました。BME280が外気に触れるように,また,タクトスイッチのボタンが外に出るように,プラスチックの窓に穴をあけています。綺麗な丸い穴をあけられなかったため,四角い穴をカッターナイフであけています。

ソフトウェア構成

全体概要

ソフトウェアについては,MPLAB MCCにてPICマイコンの各種初期設定を行うヘッダファイル・ソースファイルを生成した上で:

  • キャラクLCDを制御する部分(ST7032.h, ST7032.c)
  • BME280から温度,湿度,気圧を読み取る部分(BME280.h, BME280.c)
  • 二進数を10進数文字列に変換する部分(numstringify.h, numstringify.c)

を作りました。

main.cの中ではタクトスイッチが押されたことによる割り込みハンドラを作り,その中でスリープと復帰のフラグを管理しています。また,main関数の中で,BME280から読み取った温度,湿度,気圧を文字列に変換し,キャラクLCDに表示するという一連の流れを0.5秒毎に繰り返す処理を無限ループ内に記述しています。この無限ループの中で,スリープのフラグが経っていれば,キャラクLCDへの電源を遮断した上でPICマイコンをスリープモードさせる処理,また,復帰後した場合にはキャラクLCDへの給電を開始し,再び初期化する処理を記述しています。

MPLAB X IDE 6.25で作成したのプロジェクトの全体を下記に置きましたので,よろしければご参照下さい。

github.com

MPLAB MCCの設定

PIC16F18326を使用するにあたり,MPLAB MCCにてピンやペリフェラルの設定をします。MPLAB MCCはPICマイコンなど,Microchip社のマイコンのさまざまな設定をGUIで行うことができるツールであり,最終的にプロジェクトに必要な設定を記述したヘッダファイル(.h)とソースファイル(.c)を書き出してくれます。STM32マイコンにおけるSTM32CubeMXとコンセプトは同じですね。昔はこれらをすべて相当するレジスタを直接叩いて設定していた(しかもアセンブラ)のですから,隔世の感がありますね。

クロックの設定

図7にクロックの設定を示します。PIC16F18326は内部クロックにて最大32 MHz(4クロックで1命令を実行)で動作させることができますが,今回はボタン電池駆動であるため,低消費電力化を狙って4 MHzに設定しました(HFINTOSC = 4 MHz)。まぁ,1 MHzでも良かったのかもしれませんが,それは今後試してみようと思います。

図7: MPLAB MCCにおけるクロックの設定

使用するペリフェラルの選択

今回の気圧計のためにPIC16F18326で使用するペリフェラルを選びます。図8にMPLAB MCCのリソースマネージャを示します。リソースマネージャからI2C1_Host, DELAY, TMR1を使用できるように選びました。I2C1_Hostは後述のようにキャラクLCDと気圧センサBME280の通信の用います。I2C1_Hostを選ぶとMSSP1が自動的にペリフェラルとして追加されます。表現が難しいですが,図9のように,MSSP1の中にI2C1_Hostが入ったような絵が表示されます。

図8: MPLABのリソースマネージャ
DELAYは指定時間待つ関数「DELAY_milliseconds()」を使用するために必要です(PICマイコンを使うようなほとんどの用途で必要ではないでしょうか)。TMR1は0.5秒毎に温度,湿度,気圧を測定するために使用します。
図9: MPLAB MCCのシステム構成全体図

MSSPの設定

キャラクLCDと気圧センサBME280はともにPICマイコンとI2Cで接続します*3。多くのPICマイコンでは,I2CとSPIに対応するシリアル通信用のペリフェラルとしてMSSPが設けられています。PIC16F18326にはMSSPが2つあり,そのうち今回はMSSP1のみを用います。図9にMSSPの設定を示します。ここではデフォルトのまま何も変えておりません。注意すべきは,Interrupt Drivenがオンになっていることですね。後述しますが,main.cの中で割り込みをイネーブルにしないとI2Cが使えません。MCCから自動生成されたmain.cではデフォルトで割り込みを使用しないようになっています。何でやねん…。

図9: MPLAB MCCによるMSSPの設定
図10にI2C_Hostの設定を示します。Requested Speedを400 kHzに設定しましたが,実際は250 kHzになっています(100 kHzに設定しても良かったかもしれません)。
図10: MPLABによるI2Cホストの設定

Timer 1の設定

筆者もよく理解していないのですが,恐らく上述のDELAY_milliseconds()関数はTimer 0を利用していると思われます。そこで,0.5秒毎の測定にはTimer 1を利用することにしました。

図11: MPLAB MCCによるTimer 1の設定
Timer 1は16 bitのタイマーであり,プリスケーラをうまく設定すれば,0.5秒(500 ms)を作り出せそうです。図11に設定を示します。使用するのでもちろんTimer Enableをオンにします。Clock SourceをFOSC/4 = 1 MHzに設定しました。プリスケーラを1/8としたので,1 MHz / 8 = 125 kHz,つまり8 μsに1つずつカウントアップしていきますね。62,500カウントすれば500 ms (= 8 μs \times; 62,500 = 500,000 μs)となります。16ビットカウンタなので,65,535以下であれば設定可能です。MPLAB MCCではカウント値ではなく時間を直接指定することが可能で,図11でも500 msを設定しました。自動的に近い時間を得るためのカウント値が計算されます(図11の3,036は関係ない値と思われます)。

ピンの設定

LEDに接続されているピン(RA2),キャラクLCDへの電源供給を担うピン(RC2, RC3),タクトスイッチに接続されているピン(RA1)を含む各ピンを設定します。図12にピングリッドを示します。MSSP1 (I2C1_Host)で使用するRC0, RC1は自動的に設定されています。リセット(MCLR)ピンとなるRA3も同様です。ここではRA2とRC2, RC3をそれぞれGPIOの出力(iutput)に設定し,RA1を入力(input)に設定します。

図12: MPLAB MCCのピングリッド
少々分かりにくいですが,南京錠が閉まっている機能が「そのピンを占有している」という意味のようです。なお,タクトスイッチに繋がるRA1はICSPのためのICSPCLKと共用になっています*4

図13にピンの詳細設定を示します。ここではI2CやGPIOとして使用するピンの各種設定が可能です。I2CのSCL, SDAとして使用するRC0, RC1については,Weak Pullupを有効にしました。こうすることによって,PIC内部にプルアップ抵抗(数十kΩのようです)が設けられるため,外付けのプルアップ抵抗を省略できます。

図13: MPLAB MCCにおけるピンの詳細設定
タクトスイッチに繋がるRA1も同様にWeak Pullupを有効にします。プルアップ抵抗が内蔵となり,RA1ピンには押された際にGNDに落とすタクトスイッチのみを繋げれば良いため,なかなか便利です。また,タクトスイッチが押された際に割り込みを発生させたいので,Interrupt on Change (IOC)をnegtiveに設定します。

キャラクLCDの電源供給に用いるRC2とRC3はLOW出力時の電流シンクとしてしか使用しませんので,Open Drainに設定します。

それぞれのピンには名前を付けることができます。LEDを点灯させるためのRA2にはLED,I2CのSCLとSDAとして使用するRC0, RC1はそのままそれぞれSCL, SDAという名前を付けました。また,タクトスイッチに繋がるRA1にはSW,キャラクLCDの電源供給に使用するRC2, RC3はそれぞれLCD_Power, LCD_Power_2という名前を付けました*5

コードの生成

MPLAB MCCでの各種設定を行った後は,画面左上のGenerateボタンを押せば,設定を反映したコード生成を生成してくれます。基本的にユーザはmain.cの中にアプリケーションのコードを書いていくことになります。一点,上述のようにI2Cに割り込みを使用する場合,割り込みを有効にするよう,手動でmain.cを書き換える必要があります。

    SYSTEM_Initialize();
    // If using interrupts in PIC18 High/Low Priority Mode you need to enable the Global High and Low Interrupts 
    // If using interrupts in PIC Mid-Range Compatibility Mode you need to enable the Global and Peripheral Interrupts 
    // Use the following macros to: 

    // Enable the Global Interrupts 
    INTERRUPT_GlobalInterruptEnable(); 

    // Disable the Global Interrupts 
    //INTERRUPT_GlobalInterruptDisable(); 

    // Enable the Peripheral Interrupts 
    INTERRUPT_PeripheralInterruptEnable(); 

    // Disable the Peripheral Interrupts 
    //INTERRUPT_PeripheralInterruptDisable(); 

ここでは7行目と13行目を活かし,10行目と16行目をコメントアウトしています(生成時は逆になっています)。ここがMPLAB MCCによる自動生成ではないのが謎ですね。

キャラクLCDの制御

秋月電子通商の16桁 × 2行のキャラクLCDは,小型,I2C接続,ピッチ変換不要(もともと2.54 mmピッチ),比較的安価とこれからの電子工作に最適な選択肢の1つです。同様のコンセプトの表示デバイスとして小型のグラフィックOLEDがありますが,グラフィックOLEDで文字を表示しようとするとソフトウェアでフォントを持っている必要があり,Raspberry PiやESP32と比較するとプログラムメモリが小さなPICマイコンにはやはりキャラクLCDが適していますね。レトロな雰囲気,バックライトとの組み合わせによってはVFDのような風情も感じられて,筆者としてはなかなか気に入っています。

PICマイコンにてこのキャラクLCDを制御するライブラリがないか探そうとしましたが,mbedやArduino IDEと異なり,ライブラリを探すのが容易ではありません。そこで,かつてArduino IDE用に自作したライブラリをPICマイコン用に移植(と言ってもC++とCで異なる部分多し…)することにしました。I2Cの通信に関してはMPLAB MCCが生成した関数を使用します。C++ではないのでオブジェクトという概念が.ありません…。MSSPのI2C1の各種機能は「I2C1_」で始まる様々な関数を呼ぶことで使用できます。同様に,ここで作成したキャラクLCDの各種機能も「ST7032_」で始まる関数を呼ぶことで使用できるように作りました。

短いプログラムなので全文を載せてしまいます。

ST7032.h
/*
 * ST7032.h - Library for controlling ST7032-based LCD, header file
 * (c) 2022-2025 @RR_Inyo
 */

#ifndef ST7032_H
#define    ST7032_H

#ifdef __cplusplus
extern "C" {
#endif

// Constants (I2C address, register addresses, etc.)
#define ST7032_ADDRESS          0x3e
#define ST7032_COMMAND          0x00
#define ST7032_DATA             0x40

#define ST7032_CLEAR_DISPLAY    0x01
#define ST7032_RETURN_HOME      0x02
#define ST7032_ENTRY_MODE_I     0x06
#define ST7032_DISPLAY_ON       0x0c
#define ST7032_DISPLAY_OFF      0x08
#define ST7032_TWO_LINE         0x38
#define ST7032_TWO_LINE_IS1     0x39
#define ST7032_INTOSC_BIAS      0x14
#define ST7032_FOLLOWER_CTRL    0x6d
#define ST7032_SET_DDRAM_ADDR   0x80
#define ST7032_SET_CONTRAST_H   0x54
#define ST7032_SET_CONTRAST_L   0x70

#define ST7032_DEFAULT_CONT     0x1f

#define N_BUF 16

// Function prototypes
void ST7032_WriteCommand(uint8_t cmd);
void ST7032_WriteData(uint8_t data);
void ST7032_SetContrast(uint8_t cntr);
void ST7032_SetCursor(uint8_t loc);
void ST7032_DisplayOn(void);
void ST7032_DisplayOff(void);
void ST7032_ClearDisplay(void);
void ST7032_Initialize(void);
void ST7032_PutChar(char c);
void ST7032_PutString(char* buf);

#ifdef __cplusplus
}
#endif

#endif  /* ST7032_H */
ST7032.c
/*
 * ST7032.c - Library for controlling ST7032-based LCD, source file
 * (c) 2022-2025 @RR_Inyo
 */

#include "mcc_generated_files/system/system.h"
#include "mcc_generated_files/i2c_host/mssp1.h"
#include "mcc_generated_files/timer/delay.h"
#include "ST7032.h"

// Functions to control ST7032-based character LCD
void ST7032_WriteCommand(uint8_t cmd)
{
    uint8_t buf[2];
    buf[0] = ST7032_COMMAND;
    buf[1] = cmd;
    while(I2C1_IsBusy()) {
        // Wait until I2C bus is available
    }
    I2C1_Write(ST7032_ADDRESS, buf, 2);
    DELAY_microseconds(600);
}

// Functions to control ST7032-based character LCD
void ST7032_WriteData(uint8_t data)
{  
    uint8_t buf[2];
    buf[0] = ST7032_DATA;
    buf[1] = data;
    while(I2C1_IsBusy()) {
        // Wait until I2C bus is available
    }
    I2C1_Write(ST7032_ADDRESS, buf, 2);
    DELAY_microseconds(600);
}

void ST7032_SetContrast(uint8_t cntr)
{
    uint8_t cntr_L, cntr_H;
    cntr_L = cntr & 0b00001111;
    cntr_H = (cntr & 0b00110000) >> 4;
    ST7032_WriteCommand(ST7032_TWO_LINE_IS1);
    ST7032_WriteCommand(ST7032_SET_CONTRAST_L | cntr_L);
    ST7032_WriteCommand(ST7032_SET_CONTRAST_H | cntr_H);
    ST7032_WriteCommand(ST7032_TWO_LINE);
}

void ST7032_SetCursor(uint8_t loc)
{
    ST7032_WriteCommand(ST7032_SET_DDRAM_ADDR | loc);
}

void ST7032_DisplayOn(void)
{
    ST7032_WriteCommand(ST7032_DISPLAY_ON);
}

void ST7032_DisplayOff(void)
{
    ST7032_WriteCommand(ST7032_DISPLAY_OFF);
}

void ST7032_ClearDisplay(void)
{
    ST7032_WriteCommand(ST7032_CLEAR_DISPLAY);
}

// Function to initialize ST7032-based character LCD
void ST7032_Initialize(void)
{
    // Wait for 50 ms
    DELAY_milliseconds(50);
    
    // Initialize ST7032-based character LCD
    ST7032_WriteCommand(ST7032_TWO_LINE);
    ST7032_WriteCommand(ST7032_TWO_LINE_IS1);
    ST7032_WriteCommand(ST7032_INTOSC_BIAS);
    ST7032_WriteCommand(ST7032_SET_CONTRAST_L | (0b00001111 & ST7032_DEFAULT_CONT));
    ST7032_WriteCommand(ST7032_SET_CONTRAST_H | ((0b00110000 & ST7032_DEFAULT_CONT) >> 4));
    ST7032_WriteCommand(ST7032_FOLLOWER_CTRL);
    DELAY_milliseconds(200);
    ST7032_WriteCommand(ST7032_TWO_LINE);
    ST7032_WriteCommand(ST7032_DISPLAY_ON);
    ST7032_WriteCommand(ST7032_RETURN_HOME);
    ST7032_WriteCommand(ST7032_ENTRY_MODE_I);
    ST7032_WriteCommand(ST7032_CLEAR_DISPLAY);
}

void ST7032_PutChar(char c)
{
    ST7032_WriteData((uint8_t) c);                                
}

void ST7032_PutString(char* buf)
{
    for (int i = 0; i < N_BUF; i++) {
        if (buf[i] != '\0') {
            ST7032_PutChar(buf[i]);
        }
        else {
            break;
        }
    }
}

BME280の制御

Bosch Sensortecの温湿度気圧センサBME280PICマイコンとはI2Cで通信します(SPIにも対応しています)。これも同様にPICマイコン用のライブラリを探せなかったので,自分で作ることにしました。I2Cでの通信に関してはMSSPのI2C1を制御する関数群を用います。

LPS25HBなど他の気圧センサとは異なり,BME280では内部のレジスタから読み取った温度,湿度,気圧に対して,同じく内部のレジスタから読み取ったいくつもの補正係数を適用した複雑な補正計算をマイコン側でやってあげる必要があります。これらの補正係数は,そのBME280の出荷前に校正の上で書き込まれているものであり*6,これを適用しないと正しい測定値が得られません。この補正計算については,BME280のデータシート上にC言語での記述例が記載されておりますが…複雑すぎてはっきり言って何をやっているのか全く不明です?? 8ビットマイコンであるPIC16F18326で計算できるように気を付けながら(試行錯誤しながら)移植しました。

複雑すぎてもはや面白いので,内部レジスタから生値を読む部分と,そこから補正係数を適用した計算によって温度,湿度,気圧を得る部分を以下に示します。

int32_t BME280_GetTemperatureRaw(void)
{
    uint8_t buf[3];
    BME280_ReadMultiRegisters(BME280_TEMP_MSB, buf, 3);
    BME280_t_raw = ((uint32_t) buf[0]) << 12 | ((uint32_t) buf[1]) << 4 | ((uint32_t) buf[2]) >> 4;
    return BME280_t_raw;
}

int32_t BME280_GetPressureRaw(void)
{
    uint8_t buf[3];
    BME280_ReadMultiRegisters(BME280_PRESS_MSB, buf, 3);
    BME280_p_raw = ((uint32_t) buf[0]) << 12 | ((uint32_t) buf[1]) << 4 | ((uint32_t) buf[2]) >> 4;
    return BME280_p_raw;
}

int32_t BME280_GetHumidityRaw(void)
{
    uint8_t buf[2];
    BME280_ReadMultiRegisters(BME280_HUM_MSB, buf, 2);
    BME280_h_raw = ((uint16_t) buf[0]) << 8 | buf[1];
    return BME280_h_raw;
}

// Based on BME280 datasheet, Section 8.2
float BME280_GetTemperatureCelcius(void)
{
    int32_t var1, var2, t;
    int32_t adc_T = BME280_GetTemperatureRaw();
    
    var1 = ((((adc_T >> 3) - ((int32_t) BME280_dig_T1 << 1))) * ((int32_t) BME280_dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((int32_t) BME280_dig_T1)) * ((adc_T >> 4) - ((int32_t) BME280_dig_T1))) >> 12) * ((int32_t) BME280_dig_T3)) >> 14;
    BME280_t_fine = var1 + var2;
    t = (BME280_t_fine * 5 + 128) >> 8;
    return (float) t / 100.0;
}

// Based on BME280 datasheet, Section 8.2
float BME280_GetPressurehPa(void)
{
    int32_t var1, var2;
    uint32_t p;
    int32_t adc_P = BME280_GetPressureRaw();
    
    var1 = (((int32_t) BME280_t_fine) >> 1) - (int32_t) 64000;
    var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * ((int32_t) BME280_dig_P6);
    var2 = var2 + ((var1 * ((int32_t) BME280_dig_P5)) << 1);
    var2 = (var2 >> 2) + (((int32_t) BME280_dig_P4) << 16);
    var1 = (((BME280_dig_P3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + ((((int32_t) BME280_dig_P2) * var1) >> 1)) >> 18;
    var1 = ((((32768 + var1)) * ((int32_t) BME280_dig_P1)) >> 15);
    if(var1 == 1) {
        return 0;
    }
    p = (((uint32_t)(((int32_t) 1048576) - adc_P) - (var2 >> 12))) * 3125;
    if (p < 0x80000000) {
        p = (p << 1) / ((uint32_t) var1); 
    }
    else {
        p = (p / (uint32_t) var1) * 2;
    }
    var1 = (((int32_t) BME280_dig_P9) * ((int32_t) (((p >> 3) * (p >> 3)) >> 13))) >> 12;
    var2 = (((int32_t) (p >> 2)) * ((int32_t) BME280_dig_P8)) >> 13;
    p = (uint32_t) ((int32_t) p + ((var1 + var2 + BME280_dig_P7) >> 4));
    
    return (float) p / 100.0;
}

// Based on BME280 datasheet, Section 8.2
float BME280_GetHumidityPercentRH(void)
{
    int32_t v_x1_u32r;
    uint32_t h;
    uint32_t adc_H = BME280_GetHumidityRaw();
    
    v_x1_u32r = (BME280_t_fine - ((int32_t) 76800));
    v_x1_u32r = (((((adc_H << 14) - (((int32_t) BME280_dig_H4) << 20) - (((int32_t) BME280_dig_H5) * v_x1_u32r)) 
            + ((int32_t) 16384)) >> 15) * (((((((v_x1_u32r * ((int32_t) BME280_dig_H6)) >> 10) * (((v_x1_u32r 
            * ((int32_t) BME280_dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t) 2097152)) 
            * ((int32_t) BME280_dig_H2) + 8192) >> 14));
    v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t) BME280_dig_H1)) >> 4));
    v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r);
    v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r);
    h = (uint32_t) v_x1_u32r >> 12;
    return (float) h / 1024.0;
}

これがまともに動いた時には少し感動しました。

数値から文字列への変換

BME280から得た温度,湿度,気圧をキャラクLCDに表示するには,それぞれを十進数で表した文字列に変換する必要があります。普通の考えでは,stdio.hをインクルードしてsprintf関数を使うという発想になりますが,PICマイコンはプログラムメモリが小さく(大きなプログラムメモリをもつPIC16F18326でも16 kwordのみ),sprintf関数を使用しようと思うと,プログラムメモリが圧迫され,PICの型番によっては溢れてしまいます…。そこで,浮動小数点数(float型)を十進数で表した文字列に変換するという機能に限定したnumstringifyライブラリを自作しました。四捨五入せずに切り下げに限定してしまうなどの手抜き割り切りをしていますが,桁数も指定できるように作っています。

かつてはこれに類する処理をPICアセンブラで書いたことがあります(整数限定ですが)。その頃を思い出すようなプログラミング体験でしたね…。

温度,湿度,気圧としてあり得そうな値で,かつ,桁数が適切に設定されている場合しか動作を確認していませんので,一般の用途に使えるようにしようとすると,エラー処理などをプラスする必要がありそうです。

numstringify.h
/*
 * numstringify.h - A library to convert numerical values to strings, header file
 * (c) 2025 @RR_Inyo
*/

#ifndef NUMSTRINGIFY_H
#define    NUMSTRINGIFY_H

#ifdef __cplusplus
extern "C" {
#endif

void numstringify(char *buf, float num, unsigned int maxpow, unsigned int digits);

#ifdef __cplusplus
}
#endif

#endif  /* NUMSTRINGIFY_H */
numstringify.c
/*
 * numstringify.h - A library to convert numerical values to strings, header file
 * (c) 2025 @RR_Inyo
*/

#include "mcc_generated_files/system/system.h"
#include "numstringify.h"

void numstringify(char *buf, float num, unsigned int maxpow, unsigned int digits)
{
    float b = 1.0;
    for (int i = 0; i < maxpow; i++) {
        b *= 10.0;
    }

    int j = 0;
    int k = 0;
    char d = 0;
    int nonzero = 0;

    if (num < 0) {
        num = -num;
        buf[k] = '-';
        k++;
    }

    while (j <= digits) {
        num -= b;

        if (num > 0) {
            d++;
            nonzero = 1;
            continue;
        }
        else {
            num += b;
            if (nonzero > 0 || b < 10.0) {
                buf[k] = '0' + d;
                k++;
            }
            j++;
            b /= 10.0;
        }

        if (j == maxpow + 1) {
            buf[k] = '.';
            k++;
        }

        d = 0;
    }

    buf[k] = '\0';
}

割り込み処理,測定・表示ループ,そしてスリープモード

省電力化のため,タクトスイッチを押すたびにスリープモードと復帰を繰り返すようにします。スリープモードからの復帰の条件として外部割込み(ピンへの入力に起因する割り込み)があり,今回はこれを利用することにします。

上述のようにピンの設定でRA1ピン(名前: SW)の変化時の割り込み(IOC, interrupt on change)を有効,かつ立下りでの割り込み要求が発生するようにしています。PIC16Fマイコンでは割り込み発生時にはその要因にかかわらず必ずプログラムメモリの4番地に飛ぶようになっており,それ以降のユーザプログラム側で要因の判定をしてあげなければなりません。

PICアセンブラでプログラムを書いていた時代には,4番地以降に割り込みルーチンを書いていました。C言語となった今では,ユーザがプログラムメモリの番地を意識する必要がないのはもちろんのこと,MPLAB MCCが割り込み要因の判定をする部分まで自動的にコード生成してくれます。ユーザはMPLAB MCCが生成した要因別の割り込みのための関数(ISR, interrupt service handler)の中に直接処理を記述するか,別の関数をISRとして割り付けることができます。

MPLAB MCCの自動生成ファイルの中にユーザ側の処理を直接記述するのが何となく気持ち悪いため,筆者は今回,main.cの中に作ったmySW_ISR関数をISRとして割り付けることにしました。myISRの中でスリープモードするか否かを管理するフラグSLEEP_FLAGをtrueにしています。

また,main関数の後半にwhile(1)による無限ループを作り,この中で温度,湿度,気圧の測定とLCDへの表示を繰り返しますが,SLEEP_FLAGがtrueだった場合にスリープモードに入る処理を記述します。

なお,タクトスイッチが押されたことによってスリープモードに入る処理については,以下のブログ記事を参考にさせて頂きました。ただし,このブログ記事では,割り込み処理ルーチンの中でSLEEP();を呼んでいました。この場合,割り込み処理ルーチンの中でキャラクLCDにメッセージを表示できなかったので,本記事では割り込み処理ルーチンではフラグのみを処理するように変えてみました。 meideru.com

mySW_ISR関数

mySW_ISR関数は以下のようになっています。

void mySW_ISR(void)
{
    // Wait until chattering settles
    DELAY_milliseconds(20);
    
    if (!SW_GetValue()) {
        // Proceed when button if released
        DELAY_milliseconds(20);
        while (!SW_GetValue()) {
            // Do nothing here
        }
        DELAY_milliseconds(50);
        
        SLEEP_FLAG = true;
    }
    
    // Clear interrupt-on-change flag here to allow sleep        
    IOCAFbits.IOCAF1 = 0;
}

まず,いきなり20 ms待ってから,もう一度RA1ピンの電圧を読みに行きます。その際にもLOWだった場合,つまり,タクトスイッチが押されていた場合は再び20 ms待った後,さらにタクトスイッチが離されるまで待ちます。さらに50 ms待った後,スリープモードするか否かというフラグSLEEP_FLAGをtrueにします。これらのまどろっこしいウェイトはチャタリング対策です。

最後に,割り込み要求ビットを0に戻します。ここだけはレジスタを直接叩いているのでアセンブラ時代を感じさせますね。

main関数

一方,main関数の中では,以下のように記述します。7行目にて割り込み処理ルーチン(ISR)をmyISR関数に設定しています。また,11行目のwhile(1)以降で温度,湿度,気圧の測定とキャラクLCDへの表示を繰り返しています。

18行目でSLEEP_FLAGがtrueとなっていることを検知したら,各種処理の後*7で, 41行目のSLEEP();にてスリープモードに入ります。念のため,NOP();も置きました。

タクトスイッチが再び押され,もう一度割り込みが生じるとスリープモードから覚めますが,最初から起動し直すESP32とは異なり,PICマイコンの場合はSLEEP();のすぐ下からプログラムが再開します。そこで,各種処理の後でSLEEP_FLAGをfalseにしてこのif文を抜けます。

86行目でTimer 1がオーバーフローするまで,すなわち0.5秒が経過するのを待ちます。オーバーフローしたらすぐにフラグを立ち下げて,無限ループの先頭に戻り,次の測定,表示に移ります。

int main(void)
{

(中略)

    // Set interrupt handler for the push button
    SW_SetInterruptHandler(mySW_ISR);

(中略)

    while(1) {

(中略)

        // ------------------------------------------------
        // Check sleep flag and go into sleep if true
        // ------------------------------------------------
        if (SLEEP_FLAG) {
            // Turn off LED
            LED_SetLow();

            // Clear interrupt-on-change flag here to allow sleep        
            IOCAFbits.IOCAF1 = 0;
            
            // Show message to sleep
            ST7032_ClearDisplay();
            ST7032_SetCursor(0x00);
            ST7032_PutString("Going to sleep..");
            ST7032_SetCursor(0x40);
            ST7032_PutString("Bye!  (=.=)zzz  ");            
            DELAY_milliseconds(1000);
            
            // Turn off LCD power, added RC3 pin to sink current
            LCD_Power_SetHigh();
            LCD_Power_2_SetHigh();

            // Disable peripheral interrupts 
            INTERRUPT_PeripheralInterruptDisable();

            // Get into sleep mode!
            SLEEP();
            NOP();
            
            // Turn on LCD power, added RC2
            LCD_Power_SetLow();
            LCD_Power_2_SetLow();
            
            // Enable peripheral interrupts 
            INTERRUPT_PeripheralInterruptEnable();
            
            // Wait for ST7032 power-up and internal initialization
            DELAY_milliseconds(100);
            
            // Initialize ST7032-based LCD
            ST7032_Initialize();
            ST7032_SetContrast(DEFAULT_CONTRAST);
            
            // Proceed when button if released
            DELAY_milliseconds(20);
            while (!SW_GetValue()) {
                // Do nothing here
            }
            DELAY_milliseconds(50);
            
            // Show message to wake up
            ST7032_ClearDisplay();
            ST7032_SetCursor(0x00);
            ST7032_PutString("Waking up!      ");
            ST7032_SetCursor(0x40);
            ST7032_PutString("Hello!  (^o^)/  ");            
            DELAY_milliseconds(1000);
            
            // Clear LCD
            ST7032_ClearDisplay();
            
            // Clear sleep flag
            SLEEP_FLAG = false;
            
            // Clear interrupt-on-change flag here to allow sleep        
            IOCAFbits.IOCAF1 = 0;
        }

(中略)

        // Wait until Timer 1 overflows (500 ms)
        while (!TMR1_OverflowStatusGet()) {
            // Do nothing here. Just wait.
        }
        
        TMR1_OverflowStatusClear();
    }  
}

まとめ

長くなってしまいましたが,PICマイコンによるボタン電池駆動の気圧計に関するハードウェア,ソフトウェア構成をそれぞれ記述しました。ライブラリを探せなかったためにST7032(キャラクLCD)やBME280とのやり取りを自分で作りましたが,MPLABにもArduino IDEのような有志によるライブラリを簡単に探してインストールできる仕組みが欲しいですね。

ボタン電池による駆動を考慮し,タクトスイッチが押されるとPICマイコンがスリープモードに入るようにしました。割り込みルーチンではフラグを立てるのみで,main関数の方でフラグに基づいてスリープモードに入る処理を記述しています。

図14: PICマイコンによるポータブル気圧計を外に持ち出そう!

完成した窓付き缶ケース入りのポータブル気圧計はなかなか気に入ったので,時々眺めたり,外に持ち出したりしています。

*1:ただしプログラムメモリが不足していて断念しました。またPIC16F19155は28ピンもあるためC基板に全体を収めるのが難しく,採用できませんでした。8ピンでプログラムメモリ16 kwordのPICマイコンがあったら採用していたでしょう…。

*2:例えばhttps://amzn.asia/d/33x9JWNなど

*3:本当はI²Cと書いてアイ・スクウェーアド・シーと読むのですが,筆者はついアイ・ツー・シーと言ってしまいます。

*4:そのため,MPLAB SNAPにてプログラム書き込み中にタクトスイッチが押されてしまうと,書き込みが失敗します。

*5:もともとはRC2のみを使用するつもりでしたが,LOW出力時の電圧が高かったため,RC3を並列で使用することにしたという経緯があります。

*6:したがって個体差があると思われます。確認したことはありませんが…。

*7:I2Cによる割り込みが生じないよう(スリープモードできなくなってしまう)に,38行目でペリフェラルによる割り込みを無効にしています。




以上の内容はhttps://negligible.hatenablog.com/entry/2025/10/13/020750より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14