Raspberry PiでI2C接続キャラクターディスプレイを制御する/CPU温度の表示

この間買ったRaspberry Piは基本的にヘッドレス運用(キーボードもディスプレイもつながない)をしている。

ただ、CPUの温度などの、安定動作に関わる情報はリアルタイムで確認できると良い。なので、小型のLCDを取り付けてそこに表示させてみることにした。

I2Cキャラクターディスプレイ

まずは適当なI2Cキャラクターディスプレイを調達するのだが、秋月で売っているこれ

が小型で安価かつ、ラズパイで使うことを想定していて良さそうだった。

ただ、うちのラズパイ4は「ヒートシンク一体型ケース」を使っており、GPIOのピンにこのモジュールを直接挿すことができなかった。

LinuxからI2Cを使う

ラズパイ側のOSはUbuntu 20.04 LTSで作業したが、公式のRaspberry Pi OS(旧Raspbian)でも同様にできると思う。

ググった感じではラズパイでI2Cを有効にするには何か手順が必要そうだったが、うちの環境ではすでに有効になっていた(/dev/i2c-1 が存在した)

まず、接続したモジュールが認識されていることを確認する。i2c-toolsパッケージのi2cdetectコマンドを利用する。

$ sudo apt install i2c-tools
$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 3e -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         

データシートによると今回のLCDモジュールのI2Cアドレスは0x3eなので、無事に認識されているようだ。

wiringPiによるプログラミング

プログラミングには、C言語およびwiringPiを使用することにした。去年の8月を以って作者によるwiringPiの開発は終了しているようだが、現在はどういう状況なのだろう?GitHubの方には “Unofficial Mirror” というのがあって有志によるバグフィックス等が行われているようだが。

ともあれ、今回の用途ではwiringPiのI2C関連の機能をRaspberry Pi 4Bで使うのに特に問題は見られなかった。なのでこの記事でもwiringPiを使うことにする。

まずは、wiringPiの開発用ヘッダー等をaptでインストールする。GCCが入っていない場合はもちろんGCCもインストールする必要がある。

$ sudo apt install libwiringpi-dev

書いたコードは次のとおり:

#include <wiringPiI2C.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>

int writeString(int fd, const char *s)
{
  char c;
  while (c = *s++) {
    int result = wiringPiI2CWriteReg8(fd, 0x40, c);
    if (result < 0) {
      return result;
    }
    usleep(1000);
  }
  return 0;
}

int main(void)
{
  int fd = wiringPiI2CSetup(0x3e);
  if (fd == -1) {
    fprintf(stderr, "Failed to initialize I2C. errno = %d\n", errno);
    return 1;
  }
  // Function set: DL=1, N=1, DH=0, IS=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x38);
  usleep(1000);
  // Function set: DL=1, N=1, DH=0, IS=1
  wiringPiI2CWriteReg8(fd, 0x00, 0x39);
  usleep(1000);
  // Internal OSC frequency: BS=0, F2=1, F1=0, F0=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x14);
  usleep(1000);
  // Contrast set: C3=0, C2=0, C1=0, C0=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x70);
  usleep(1000);
  // Power/ICON/Contrast control: Ion=0, Bon=1, C5=1, C4=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x56);
  usleep(1000);
  // Follower control: Fon=1, Rab2=1, Rab1=0, Rab0=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x6c);
  usleep(1000 * 200);
  // Function set: DL=1, N=1, DH=0, IS=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x38);
  usleep(1000);
  // Display ON/OFF control: D=1, C=0, B=0
  wiringPiI2CWriteReg8(fd, 0x00, 0x0C);
  usleep(1000);
  // Clear Display
  wiringPiI2CWriteReg8(fd, 0x00, 0x01);
  usleep(1000 * 1000);

  writeString(fd, "Hello");
  
  // Set DDRAM address: AC=0x40
  wiringPiI2CWriteReg8(fd, 0x00, 0x80 | 0x40);

  writeString(fd, "world!");

  close(fd);
}

この内容を hello.c という名前で保存した場合は、

$ gcc -o hello hello.c -lwiringPi
$ sudo ./hello

という感じでコンパイル・実行する。

コマンドの実行は wiringPiI2CWriteReg8(fd, 0x00, <コマンドを表す8ビット値>) という感じで、データ(文字)の出力は wiringPiI2CWriteReg8(fd, 0x40, <文字を表す8ビット値>) という感じで行えるようだ。今回のコードでは後者は writeString という自作の関数の中で行っている。

コードの前半は初期化で、データシートの「初期設定例」をそのまま実行している。

説明するまでもないが、実際に文字を出力しているのは writeString の呼び出しだ。

文字を出力するにつれてカーソル位置が右に移動するが、画面の右端(8文字目の次)に到達しても自動で折り返しは行われない(64文字以上出力すれば2行目に行くのかもしれないが)。データシートで言うところのACという変数がカーソル位置を表しており、2行目に出力するにはこれを 0x40 に設定する必要がある。そのために “Set DDRAM address” コマンドに 0x40 を指定して発行している。

実行の際は、(I2Cバスというハードウェアを直接(?)触るため)特権が必要となる。そのため、 sudo ./hello という感じで実行する。

ちなみに、権限が足りない場合(sudo をつけずに実行した場合)は、

Unable to open I2C device: Permission denied

というメッセージが表示される。前述のコードに書いたメッセージとは違うことにお気づきだろうか?そう、このエラーメッセージはwiringPiが独自に出力しているのだ。しかも、我々の書いたエラーメッセージが出力されないということは、wiringPiの側でexit()を呼んでいるということになる。

ソースコードを確認した感じでは、この辺の挙動はwiringPiReturnCodesという変数で制御されている。wiringPiReturnCodes変数はwiringPiSetup関数で環境変数に応じて設定されている。

https://github.com/WiringPi/WiringPi/blob/50b7c5ed7d238a637db7d9c73978466eb022a8de/wiringPi/wiringPi.c#L658-L674

この辺の事情はドキュメントにも記載されていた。(我々のコードではwiringPiSetup関数を呼んでいないが、本当はちゃんと呼び出した方が良いのかもしれない)

CPUの温度を表示させる

Raspberry PiのCPUの温度を取得するには、vcgencmdコマンドを使うやり方と、特殊なファイル /sys/class/thermal/thermal_zone0/temp を読み出すやり方があるようだ。うちの環境はRaspberry Pi OSじゃないせいかvcgencmdが入っていない。なので、後者を利用する。

(余談だが、「raspberry pi cpu 温度」で検索すると後者のやり方を「catコマンドを使うやり方」と呼んでいるページが上位に出てきた。あのね、重要なのはcatコマンドじゃなくて…(掌を上に向け首を振る))

書いたコードは次のとおり:

#include <wiringPiI2C.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int writeString(int fd, const char *s)
{
  char c;
  while (c = *s++) {
    int result = wiringPiI2CWriteReg8(fd, 0x40, c);
    if (result < 0) {
      return result;
    }
    usleep(1000);
  }
  return 0;
}

double get_cpu_temp(void)
{
  FILE *f = fopen("/sys/class/thermal/thermal_zone0/temp", "r");
  char buf[256];
  fgets(buf, sizeof(buf), f);
  fclose(f);
  return atof(buf) / 1000.0;
}

int main(void)
{
  int fd = wiringPiI2CSetup(0x3e);
  if (fd == -1) {
    fprintf(stderr, "Failed to initialize I2C. errno = %d\n", errno);
    return 1;
  }
  // Function set
  wiringPiI2CWriteReg8(fd, 0x00, 0x38);
  usleep(1000);
  // Function set
  wiringPiI2CWriteReg8(fd, 0x00, 0x39);
  usleep(1000);
  // Internal OSC frequency
  wiringPiI2CWriteReg8(fd, 0x00, 0x14);
  usleep(1000);
  // Contrast set
  wiringPiI2CWriteReg8(fd, 0x00, 0x70);
  usleep(1000);
  // Power/ICON/Contrast control
  wiringPiI2CWriteReg8(fd, 0x00, 0x56);
  usleep(1000);
  // Follower control
  wiringPiI2CWriteReg8(fd, 0x00, 0x6c);
  usleep(1000 * 200);
  // Function set
  wiringPiI2CWriteReg8(fd, 0x00, 0x38);
  usleep(1000);
  // Display ON/OFF control
  wiringPiI2CWriteReg8(fd, 0x00, 0x0C);
  usleep(1000);
  // Clear Display
  wiringPiI2CWriteReg8(fd, 0x00, 0x01);
  usleep(1000 * 1000);

  writeString(fd, "CPU:");

  for (;;) {
    // Set DDRAM address: AC=0x40
    wiringPiI2CWriteReg8(fd, 0x00, 0x80 | 0x40);
    double temp = get_cpu_temp();
    char buf[256];
    sprintf(buf, "%.1f\xF2" "C", temp);
    writeString(fd, buf);
    sleep(1);
  }

  close(fd);
}

新たに書いた get_cpu_temp という関数が、CPUの温度を℃単位で取得する関数だ。

このプログラムは、LCDの1行目に「CPU:」と表示した後、無限ループに入る。

ループの中では、カーソル位置を2行目の先頭に設定した後、CPUの温度を取得する。取得した温度は小数点以下1位までの文字列に変換した後、LCDに表示している。

今回使うLCDには「℃」という文字は登録されていないので、上付きのマルっぽい文字 (0xF2) の後に C を出力している。C言語で "\xF2C" と書いてしまうと3桁の16進数と認識されてしまうので要注意だ。

今後の課題

「実験」ではなく長期的に使うなら、ラズパイとの接続方法や設置場所をどうにかしたい。ラズパイ本体は机の下にでも置いてケーブルで繋いだLCDを机の上に置く、という形になるだろうか。どうせやるならもっと大きいキャラクターディスプレイを使っても良いかもしれない。

ソフトウェア面では、フォアグラウンドで実行される通常のコマンドではなく、常時バックグラウンドで実行されるデーモンとして実行できると良さそうだ。systemctlで制御できる感じの。筆者はデーモンを書いた経験がないのでその辺を勉強する必要がある。

常時表示されていると良さそうなのは、CPU温度の他に、CPU使用率、メモリ使用率、ストレージ使用率、IPアドレスなどが考えられる。全部表示させるとキャラクターディスプレイでは足りなくなりそうなので、一定時間で切り替える感じになるだろうか。

参考にしたサイト

筆者は当初I2Cコマンドの送信方法が分からなくて困っていたが、このページの i2cset コマンドを使った動作確認をそのまま実行したら液晶に文字が表示されたので感動した。使っている言語やライブラリーは違うが、こちらの「使用例」でもCPUの温度を表示させている。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です