0.96インチOLEDにトレンドグラフを表示 (Arduino)
先日手に入れた0.96インチ128x64画素OLEDディスプレイですが、デモプログラムで動作確認までやりました。今回は一歩進んで、何か機能のあるものを作ってみます。
何が良いかいろいろ考えたのですが、アナログポートの電圧を折れ線グラフで表示する物、つまりトレンドグラフ表示器を作ることにしました。これを作るためには、文字や図形描画の機能を使う必要があるので、トレーニングの教材としてはちょうど良さそうです。
あと、現在化学反応式のCO2発生装置を運転中なのですが、この装置は夜間には運転を止めるようにプログラムしてあります。当然CO2の圧力が下がるのですが、その様子をグラフで確認出来ると便利なはずです。ということで、今回作る機能は、最終的にはCO2発生装置に組み込むことを考えています。
まずはハードウエアです。
▼実験に使った回路

Arduino UNO に接続して動かしている様子です。左に見える可変抵抗で入力電圧を変化させて、プログラムのデバッグを行います。
▼表示画面

実際に動いている様子です。画面の縦軸はADコンバーターの出力の 0から1023 の値をそのまま表示しています。縦軸の表示範囲は自動ズーム機能で、出来るだけ画面いっぱいに表示するようになっています。なお、縦軸の値は、最終的には測定対象の物理量にスケーリングする予定です。
あと、右上の値は最新の測定値、右下の2sは、横軸の1目盛りが2秒であることを表しています。つまり 2s/Dev.という意味です。
▼回路図

実験に使った回路です。OLEDへのI2Cインターフェイスと、テスト入力用の可変抵抗だけしかありません。アナログポートの0に、可変抵抗から0-5Vの電圧を加えてプログラムの動作テストに使っています。
以下が今回作ったプログラムです。ちょっと長いですが、全部見える形で掲載します。
以下プログラムのポイントを説明します。なお、私はこういうグラフィック表示のデバイスを使うのは今回が初めてです。ということで、何か変なことを書いてしまうかも知れませんがご容赦ください。
◆プログラムの全体
・ライブラリにはAdafruit_GFX.hとAdafruit_SSD1306.hを使いました。
・データー読み込みはMsTimer2を使った割込みでタイミングを決めているので、正確な時間間隔になっています。ちなみに最速は60msまで。
◆OLEDディスプレイ関係
・各種の描画コマンドがありますが、基本的には画素をONにする方向にだけ作用します。同じ位置に違う文字を書くと、両者がORされたイメージ、つまり重ね書きになります。
・実際に表示が変わるのは display.display(); コマンド (53行目) の実行後。つまりあれこれ描画コマンドを送っても、その時点では画面は変化しません。
・display.display(); の実行には37msもかかるので、処理を高速化したい場合の足かせになります。バンク単位の書き込みをしていたら、この時間はもっと早くなるのかも知れません。なお、主な画面操作関係の関数の実行時間測定結果をコメントの( )内に記入しておきました。
・ということで、たとえ1画素だけ反転させたい場合でも最低37msかかります(たぶん)。
・全部書き直すなら、一旦RAMを全部クリアして、その後に図形を書いて、最後にdisplay.display(); とやることになります。今回のプログラムではそれに近いことをやっているので、loopを廻る速度は最高で52msくらいかかっています。
・文字を書く場合、一旦ブランクを書いて消して、その後新しい文字を書く。(120-123行)
◆動画 (?rel=0)
入力値を適当に変えた時の表示の様子です。グラフ範囲の最大最小を検出して自動的にスケーリングしています。
◆まとめ
ということで、このOLEDディスプレイの使い方がほぼマスター出来た気がします。サイズは小さいものの、値段が安くて表現力が高いので積極的に使って行きたいと思います。
コントローラーのSSD1306というチップには、図形や文字の描画などの機能があるものと思っていました。しかし、そういう機能は無いようで、単にVRAMへの転送インターフェイスを提供しているだけみたいです(?)。ただ、バンク単位の書き込みが出来るようなので、そのあたりをうまく使えば、数倍高速化出来るのかも知れません。あと、もちろんOLEDを光らせるためのドライバ機能があります。
この記事のプログラムではデーターの読み取り間隔を60msにしていますが、この間隔をもっと長くすれば1日分の記録を採るような使い方が出来ます。最初の方にちょっと書いた、CO2ガス発生装置の圧力のグラフは、そういう設定で使うことになると思います。
ところで、今回のプログラムは、データーを1つ読む毎にグラフを描き直しているので高速な記録は出来ません。これを少し変えて、先にまとめてデーターを読んでおいて、後でグラフを描くようにすれば、高速な記録が可能となるはずです。つまり、オシロスコープが作れるはずです。これって面白そうなので、やってみたいと思います。
何が良いかいろいろ考えたのですが、アナログポートの電圧を折れ線グラフで表示する物、つまりトレンドグラフ表示器を作ることにしました。これを作るためには、文字や図形描画の機能を使う必要があるので、トレーニングの教材としてはちょうど良さそうです。
あと、現在化学反応式のCO2発生装置を運転中なのですが、この装置は夜間には運転を止めるようにプログラムしてあります。当然CO2の圧力が下がるのですが、その様子をグラフで確認出来ると便利なはずです。ということで、今回作る機能は、最終的にはCO2発生装置に組み込むことを考えています。
まずはハードウエアです。
▼実験に使った回路

Arduino UNO に接続して動かしている様子です。左に見える可変抵抗で入力電圧を変化させて、プログラムのデバッグを行います。
▼表示画面

実際に動いている様子です。画面の縦軸はADコンバーターの出力の 0から1023 の値をそのまま表示しています。縦軸の表示範囲は自動ズーム機能で、出来るだけ画面いっぱいに表示するようになっています。なお、縦軸の値は、最終的には測定対象の物理量にスケーリングする予定です。
あと、右上の値は最新の測定値、右下の2sは、横軸の1目盛りが2秒であることを表しています。つまり 2s/Dev.という意味です。
▼回路図

実験に使った回路です。OLEDへのI2Cインターフェイスと、テスト入力用の可変抵抗だけしかありません。アナログポートの0に、可変抵抗から0-5Vの電圧を加えてプログラムの動作テストに使っています。
以下が今回作ったプログラムです。ちょっと長いですが、全部見える形で掲載します。
/* データートレンドモニタ(20190127_OLEDgraphTest.ino)
* 0.96インチOLEDにアナログ入力の変化グラフを書く
* 2019/01/27 ラジオペンチ http://radiopench.blog96.fc2.com/
*/
#include <Wire.h>
#include <Adafruit_GFX.h> // adafruitのライブラリを使用
#include <Adafruit_SSD1306.h>
#include <MsTimer2.h> // 定周期割込みに使用
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
int latestData;
int dataBuff[110]; // データーバッファ
char chrBuff[20]; // 表示フォーマットバッファ
int dataMin;
int dataMax;
volatile boolean timeFlag = LOW;
void setup() {
pinMode(13, OUTPUT);
Serial.begin(115200);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
for (int i = 0; i <= 99; i++) {
dataBuff[i] = -1; // バッファを未定義フラグ(-1)で埋める
}
drawCommons(); // 共通部分を描画
MsTimer2::set(60, timeUp); // 60ms秒毎にタイマー割込み
MsTimer2::start();
}
void loop() {
while (timeFlag == LOW) { // MsTimer2割込み待ち
}
timeFlag = LOW;
// digitalWrite(13, HIGH);
latestData = analogRead(0); // アナログポート0のデーターを読む(115us)
saveBuff(); // 表示バッファに書き込み(190us)
dispNewData(); // 最新値を表示(1.42ms)
plotData(); // グラフプロット(7.4ms)
scaleLine(); // 目盛り線表示(1.9ms)
dispVscale(); // 縦軸目盛り表示(3.9ms)
display.display(); // バッファの値を転送して表示(37ms)
// digitalWrite(13, LOW); // 処理時間合計=52ms
}
void drawCommons() { // 共通図形の作画
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(WHITE); // Draw white text
display.setCursor(0, 0); // Start at top-left corner
display.println("Trend monitor");
display.drawRect(26, 9, 102, 55, WHITE); // グラフ領域枠の作画
display.drawFastHLine(24, 9, 2, WHITE); // Max値の補助マーク
display.drawFastHLine(24, 36, 2, WHITE); // center
display.drawFastHLine(24, 63, 2, WHITE); // Min
display.display(); // VRAM転送
}
void dispVscale() { // 縦軸目盛り表示
display.fillRect(0, 9, 24, 55, BLACK); // 前の表示をまとめて消す(24x55ドット)(460us)
display.setCursor(0, 9);
sprintf(chrBuff, "%4d", dataMax); display.print(chrBuff); // Max値表示
display.setCursor(0, 33);
sprintf(chrBuff, "%4d", (dataMax + dataMin) / 2); display.print(chrBuff); // 中心値表示
display.setCursor(0, 57);
sprintf(chrBuff, "%4d", dataMin); display.print(chrBuff); // Min値表示
}
void saveBuff() { // データバッファの更新と最大・最小値の決定
int d;
dataMin = 1023; // 最小
dataMax = 0; // 最大値記録変数を初期化
for (int i = 98; i >= 0; i--) { // 配列に値を保存しながら最大と最小値を求める
d = dataBuff[i];
dataBuff[i + 1] = d; // 配列のデーターを一つ後ろにずらし
if (d != -1) { // ずらしたデータが有効値だったら、
if (d < dataMin) { // 最小と
dataMin = d;
}
if (d > dataMax) { // 最大値を記録
dataMax = d;
}
}
}
dataBuff[0] = latestData; // 配列の先頭には最新データーを記録し、
if (latestData < dataMin) { // 最小と
dataMin = latestData;
}
if (latestData > dataMax) { // 最大値を再確認
dataMax = latestData;
}
dataMin = dataMin - 20; // 最小値を-20下に設定
dataMin = (dataMin / 10) * 10; // 10ステップに丸め
if (dataMin < 0) {
dataMin = 0; // 但し下限は0
}
dataMax = dataMax + 20; // 最大値を+20上に設定
dataMax = ((dataMax / 10) + 1) * 10; // 切り上げで10ステップに丸め
if (dataMax > 1020) {
dataMax = 1023; // 但し1020以上なら1023で抑える
}
// Serial.print(dataMin); Serial.print(", "); Serial.println(dataMax);
}
void dispNewData() { // 最新データーの値を画面の右上に表示
display.fillRect(104, 0, 24, 8, BLACK); // 前の表示値を消す(24x8ドット)(320us)
display.setCursor(104, 0);
sprintf(chrBuff, "%4d", latestData); // 4桁右詰めで
display.print(chrBuff); // 最新測定値を表示
}
void plotData() { // 配列の値に基づきデーターをプロット
long yPoint;
display.fillRect(27, 10, 100, 53, BLACK); // グラフ表示領域をクリア(100x53ドット)(1.9ms)
for (int i = 0; i <= 98; i++) {
if (dataBuff[i] == -1) { // データーが未定(-1)なら
break; // プロット中止
}
yPoint = map(dataBuff[i], dataMin, dataMax, 63, 9); // プロット座標へ変換
display.drawPixel(125 - i, yPoint, WHITE); // データをプロット
}
}
void scaleLine() { // 目盛り線を作画
for (int x = 26; x <= 128; x += 4) {
display.drawFastHLine(x, 36, 2, WHITE); // 中心線を点線で描く
}
for (int x = (127 - 33); x > 30; x -= 33) {
for (int y = 9; y < 63; y += 4) {
display.drawFastVLine(x, y, 2, WHITE); // 縦線を点線で2本描く
}
}
display.setCursor(96, 55);
display.print("< 2s>"); // 横軸スケール表示
}
void timeUp() { // MsTimer2割込み処理
timeFlag = HIGH;
}
以下プログラムのポイントを説明します。なお、私はこういうグラフィック表示のデバイスを使うのは今回が初めてです。ということで、何か変なことを書いてしまうかも知れませんがご容赦ください。
◆プログラムの全体
・ライブラリにはAdafruit_GFX.hとAdafruit_SSD1306.hを使いました。
・データー読み込みはMsTimer2を使った割込みでタイミングを決めているので、正確な時間間隔になっています。ちなみに最速は60msまで。
◆OLEDディスプレイ関係
・各種の描画コマンドがありますが、基本的には画素をONにする方向にだけ作用します。同じ位置に違う文字を書くと、両者がORされたイメージ、つまり重ね書きになります。
・実際に表示が変わるのは display.display(); コマンド (53行目) の実行後。つまりあれこれ描画コマンドを送っても、その時点では画面は変化しません。
・display.display(); の実行には37msもかかるので、処理を高速化したい場合の足かせになります。バンク単位の書き込みをしていたら、この時間はもっと早くなるのかも知れません。なお、主な画面操作関係の関数の実行時間測定結果をコメントの( )内に記入しておきました。
・ということで、たとえ1画素だけ反転させたい場合でも最低37msかかります(たぶん)。
・全部書き直すなら、一旦RAMを全部クリアして、その後に図形を書いて、最後にdisplay.display(); とやることになります。今回のプログラムではそれに近いことをやっているので、loopを廻る速度は最高で52msくらいかかっています。
・文字を書く場合、一旦ブランクを書いて消して、その後新しい文字を書く。(120-123行)
◆動画 (?rel=0)
入力値を適当に変えた時の表示の様子です。グラフ範囲の最大最小を検出して自動的にスケーリングしています。
◆まとめ
ということで、このOLEDディスプレイの使い方がほぼマスター出来た気がします。サイズは小さいものの、値段が安くて表現力が高いので積極的に使って行きたいと思います。
コントローラーのSSD1306というチップには、図形や文字の描画などの機能があるものと思っていました。しかし、そういう機能は無いようで、単にVRAMへの転送インターフェイスを提供しているだけみたいです(?)。ただ、バンク単位の書き込みが出来るようなので、そのあたりをうまく使えば、数倍高速化出来るのかも知れません。あと、もちろんOLEDを光らせるためのドライバ機能があります。
この記事のプログラムではデーターの読み取り間隔を60msにしていますが、この間隔をもっと長くすれば1日分の記録を採るような使い方が出来ます。最初の方にちょっと書いた、CO2ガス発生装置の圧力のグラフは、そういう設定で使うことになると思います。
ところで、今回のプログラムは、データーを1つ読む毎にグラフを描き直しているので高速な記録は出来ません。これを少し変えて、先にまとめてデーターを読んでおいて、後でグラフを描くようにすれば、高速な記録が可能となるはずです。つまり、オシロスコープが作れるはずです。これって面白そうなので、やってみたいと思います。
- 関連記事
-
- シリアル通信 (Serial.print) は RAM を大量に消費する (Arduino)
- PROGMEMの使い方 (Arduino)
- 0.96インチOLEDにトレンドグラフを表示 (Arduino)
- 0.96インチ 128x64ドット OLEDディスプレイ
- LOLシールドを使った LEDアレイプロジェクターの製作