ラズパイPicoタッチパネル液晶 ILI9341でSDカードの使い方

ILI9341 SDカードの使い方アイキャッチ

「Raspberry Pi Pico」でタッチパネル液晶表示器「ILI9341」に搭載されている「SDカードリーダー」の使い方を、開発環境に「ArduinoIDE」を使って詳しく紹介します。

前回、タッチパネル液晶表示器「ILI9341」の使い方を紹介しましたが、その時に使用した「ILI9341」には「SDカードリーダー」も実装されていました。
「SDカードリーダー」は「SPI通信」で、送受信端子を液晶画面と共通で使用できるため、せっかくならこれも使えるようにしておきたい!
ということで「SDカードリーダー」の使い方も紹介しておきます。
ここではSDカードリーダーの配線方法から、SDカードへのデータの読み書きや削除、JPEG画像を読み込んで液晶画面に表示する方法を確認し、最後に「サンプルプログラム」で実際にSDカードのデータ操作、画像表示を行う方法まで紹介します。
マイコンボードには「Raspberry Pi Pico2」を使用しましたが、ボード情報を変更すれば「Pico」でも「PicoW」でも同じように使用できます。

タッチパネル液晶表示器「ILI9341」の使い方や「ラズパイPico」の基本的な使用方法、「ArduinoIDE」を使用した開発環境の準備等は、以下のリンクで詳しく紹介しています。

ラズパイPicoでタッチパネル液晶表示器 ILI9341の使い方
Raspberry Pi PicoでILI9341を使用した液晶表示とタッチパネルの使い方をArduinoIDEでお絵描きやボタン操作、値表示しながら詳しく紹介
ラズパイPico/Pico2/PicoWのArduinoIDE2のインストール方法や使い方紹介
Raspberry Pi Pico/Pico2/PicoWのC言語での開発環境 ArduinoIDE2のインストールから初期設定、使い方、Lチカで動作確認まで詳しく紹介
スポンサーリンク

1.Raspberry Pi Picoとは

Raspberry Pi Pico(ラズパイ Pico)とは、イギリスのRaspberry Pi財団が開発したマイコンボードで、「Python」や「C言語」でプログラムすることができます。

本体基板上の入出力端子に、スイッチやセンサ、各種制御モジュール、通信モジュールを接続することで、それらをプログムによって制御することができ、プログラミング学習やホビー、組込み用途に最適です。


「ラズパイ Pico」の端子配列は以下のようになります。
(「Pico2」やWi-Fi通信機能搭載の「Pico W」もLED以外の基本的な端子配列は同じです。)

ラズパイ(Raspberry Pi) Pico 端子配列
LEDの端子番号については「LED_BUILTIN」で指定することで「Pico/Pico2/PicoW」それぞれのLED番号が自動で設定されます。

「ラズパイ Pico」の使い方や端子機能、開発環境の準備については、以下のリンク先で詳しく紹介しています。

ラズパイPico/PicoWの使い方を3つの開発環境Python、ArduinoIDE、PlatformIOで紹介
Raspberry Pi Pico/Pico Wの使い方を端子配列からPython(MicroPython)とC言語の開発環境、Lチカ方法まで紹介。PythonはTonny、C言語はArduinoIDEとPlatformIOの3種類で詳しく紹介します。
スポンサーリンク

2.タッチパネル液晶表示器 ILI9341とは

「ILI9341」とは、タッチパネル搭載のTFT液晶表示器で、今回使用するのは2.8インチ(320x240Pixel)のものです

外観は下画像のようになります。

TFT液晶表示器ILI9341の外観

今回購入したものの梱包状態は上画像になります。
タッチパネル機能のない(制御ICが実装されていない)ものもあるようなので、注意して購入しましょう。

TFT液晶表示器ILI9341の外観

タッチペンが付属していましたが、全てに付属はしてないかもしれません。なくてもいいと思いましたが、動作確認するとスマホのような精度はないので、タッチペンは必須と思います。

TFT液晶表示器ILI9341の外観

裏面にタッチパネルの制御ICが実装されています。
今回実装されていた制御ICは「XPT2046」でした。
SDカードリーダーも実装されていますが、通信用の端子は実装も付属もされていません。

Raspberry Pi Pico とILI9341 SDカードリーダーの使い方

SDカードリーダーを使用するには、通信用端子(上画像左部)へピンヘッダー等を半田付けします。
SDカードは上画像のようにカードリーダーに差し込みますが半分以上がはみ出します。

タッチパネルの制御ICは「MSP2807」が実装されているものもあるようなので、この場合はここのサンプルプログラムでは動作しないかもしれません。
電源電圧は「3.3V〜5.0V」ですが、制御入力は3.3Vなのでご注意ください。

今回購入したものは以下のアマゾンのものです。

スポンサーリンク

3.配線図

配線図は下画像のようになります。
SPI通信のデータ送受信配線の「MOSI」と「MISO」クロック信号の「SCK」は液晶表示とタッチパネル、SDカードリーダーで共通で、CS端子で切り替えて使用します。

Raspberry Pi Pico とILI9341 SDカードリーダーの配線図
使用する端子構成は合計10本で以下のようになります。
電源2本:3.3V(5V可)、GND(0V)
SPI通信端子3本(共通):MOSI(送信)、MISO(受信)、SCK(クロック)
チップセレクト端子3本:CS(SPI通信相手選択)
液晶表示専用端子2本:D/C(データ/コマンド切替)、LED(バックライト)
タッチパネルのSPI通信端子は「T_DIN = MOSI」、「T_DO = MISO」になります。
「T_IRQ」はタッチが検出された時の割り込み信号で、タッチ時にLOWレベルになるものですが、今回は使用しません。

実際に配線した様子は下画像のようになります。
ブレッドボードを横に3枚連結して、ジャンパー線で接続しています。

Raspberry Pi Pico とILI9341 SDカードリーダーの配線方法

SDカードリーダーの配線が見えないので、ILI9341を取り外すと下画像のようになります。

Raspberry Pi Pico とILI9341 SDカードリーダーの配線方法

ブレッドボードの端子間隔を合わせるために下画像のように、ブレッドボードの電源用2列を、裏のテープをカッターで切って取り外して組み替えています。

Raspberry Pi Pico とILI9341 SDカードリーダーの配線方法

今回使用したブレッドボードはサンハヤト製の6列タイプの以下のものです。

4.開発環境(ArduinoIDE)、ライブラリの準備

開発環境「ArduinoIDE」のインストールやライブラリの準備方法は以下のリンクで詳しく紹介しています。

Arduino IDE 2のインストール方法、初期設定、使い方
バージョンアップして使いやすくなったArduino IDE 2のインストールから使い方まで詳しく紹介、便利な機能やM5Stack、ラズパイPicoでの使用方法も紹介します。

使用するライブラリは以下表のようになります。
「ArduinoIDE」で事前にインストールしておいてください。

ライブラリ名用途バージョン検索名
Adafruit GFX Library文字フォントの指定や画面の
チラツキ防止の「スプライト」用
1.11.1gfx
Adafruit BusIOAdafruit GFXライブラリを使用するために必要1.16.2busio
Adafruit ILI9341液晶表示器ILI9341の制御用1.6.19341
XPT2046_Touchscreenタッチパネル制御用1.4xpt paul
JPEGDecoderJPEG画像デコード処理用2.0.0jpegdeco
バージョンは動作確認時のものです。

ライブラリはプログラムの初めにヘッダーとして以下のように書いて呼び出して使用します。

#include <Adafruit_GFX.h>         // Adafruitのグラフィックスライブラリ
#include <Adafruit_ILI9341.h>     // 液晶表示器 ILI9341 制御用ライブラリ
#include <XPT2046_Touchscreen.h>  // タッチパネル制御用ライブラリ
#include <JPEGDecoder.h>          // JPEG画像デコーダー用のライブラリ
Adafruit BusIOはヘッダーに書く必要はありません。

5.microSDカードリーダーの使い方

microSDカードの操作はArduino標準ライブラリの「SD」ライブラリを使用します。
以下から使い方の例として、初期設定から書き込み、読み込み、削除、JPEG画像を読み込んで表示する方法を紹介します。

ここではSDカードを使用する方法について紹介します。
液晶表示やタッチパネルについての初期設定や使い方については、以下のリンク先で詳しく紹介しています。

ラズパイPicoでタッチパネル液晶表示器 ILI9341の使い方
Raspberry Pi PicoでILI9341を使用した液晶表示とタッチパネルの使い方をArduinoIDEでお絵描きやボタン操作、値表示しながら詳しく紹介

・初期設定

以下はSDカードリーダーを使用するための初期設定の例です。

データのやり取りは「SPI通信」で行われます。
通信端子の「MOSI(送信)/MISO(受信)/SCK(クロック)」は液晶表示とタッチパネル、SDカードリーダーで共通の端子番号を設定します。
SD_CS(チップセレクト)」端子は任意の出力端子を指定します。
この出力端子を「LOW」にすることでSDカードリーダーと通信を行うことができます。

初期設定では「SD.begin(SD_CS)」で「CS」端子番号を指定してSDカードの初期化を行なっています。

#define COMMON_SCK  18  // 液晶表示とタッチパネル、SDカードリーダー共通の SCK
#define COMMON_MOSI 19  // 液晶表示とタッチパネル、SDカードリーダー共通の MOSI
#define COMMON_MISO 16  // タッチパネル、SDカードリーダーの MISO

#define SD_CS 15  // SD(TF)カードリーダのCSピン
#define SD_FILENAME "/test.txt"          // SD(TF)カードに保存するファイル名
#define JPEG_FILENAME "/raspi_pico.jpg"  // JPEG画像ファイル名(SDカード内のJPEGファイル名を指定)

JPEGDecoder jpegDec;  // JPEGデコーダのインスタンス(JPEG画像読み込み用)
File myFile;          // Fileクラスのインスタンスを宣言

// 初期設定 ----------------------------------------
void setup() {
  Serial.begin(115200); // シリアル通信初期化

  //SPI0設定(TFT,TOUCH,SD共通)
  SPI.setTX(COMMON_MOSI); // SPI0のTX(MOSI)
  SPI.setRX(COMMON_MISO); // SPI0のRX(MISO)
  SPI.setSCK(COMMON_SCK); // SPI0のSCK

  // SDカードの初期化
  if (!SD.begin(SD_CS)) {
    Serial.println("SDカードの初期化に失敗しました");
    return;
  } else {
    Serial.println("SDカードが初期化されました");
  }
}

以下から書き込み、読み込み、削除方法を関数例としてまとめたものを紹介していきますが、いずれも上記の初期設定の例の中の以下のように「SD_FILENAME」でファイル名を指定しておきます。

#define SD_FILENAME "/test.txt"     // SD(TF)カードに保存するファイル名

さらに、「File」クラスのインスタンスを「myFile」として宣言しておきます。
これにより、「myFile」にSDカードからデータを読み込んで「myFile.〜」で指定すれば、データを操作するプログラムが実行できるようになります。

File myFile;          // Fileクラスのインスタンスを宣言

JPEG画像の読み込みについては、以下のように「JPEG_FILENAME」でjpeg画像のファイル名を指定しておきます。

#define JPEG_FILENAME "/raspi_pico.jpg"  // JPEG画像ファイル名(SDカード内のJPEGファイル名を指定)

さらに、「JPEGDecoder」のインスタンスを「jpegDec」として宣言しておきます。
これにより、「jpegDec.〜」でjpeg画像のデコード処理(ビットマップ変換)プログラムが実行できるようになります。

JPEGDecoder jpegDec;  // JPEGデコーダのインスタンス(JPEG画像読み込み用)

・ファイル書き込み例

SDカードにファイルを書き込むには以下の例のように「writeSdData()」関数を「書き込みたいデータの型」を指定して実行します。(今回は例として「”文字列”」と整数の「100」を書き込んでいます。)

writeSdData("文字列", 100); // SDカードへ「文字列」と整数100の書き込みを実行

関数例は以下になります。

void writeSdData(const char* text, int data) {
  myFile = SD.open(SD_FILENAME, FILE_WRITE); // SDカードのファイルを開く
  
  // データ書き込み
  if (myFile) { // ファイルが開けたら
    myFile.printf("%s : %d\n", text, data); // テキストと整数データを書き込み
    myFile.close(); // ファイルを閉じる
  } else { // ファイルが開けなければ
    Serial.println("ファイルを開けませんでした");
    dispMessage("Can't open file!", ILI9341_RED);  // 液晶画面メッセージ表示関数呼び出し
  }
}

書き込み処理は、まず「SD.open()」関数で「ファイル名」と「書き込みモード(FILE_WRITE)」を指定してファイルを開きます。

書き込みモードについて
FILEWRITE」は追記モードで「FILE_APPEND」と書いても同じです。
ファイルが無ければ新規作成され、ファイルがあれば既存のデータに追記されて書き込まれます。
上書きするには、この後で紹介する削除例のようにファイルを削除してから書き込みます。

ファイルが開けたら「myFile.print()」関数で書き込みデータを指定して書き込みを実行します。
書き込みが完了したら「myFile.close()」関数でファイルを閉じます。

・ファイル読み込み例

SDカードからファイルを読み込むには以下の例のように「readSdData(“ファイル名”)」関数を、読み込みたい「ファイル名」を指定して実行します。

readSdData(SD_FILENAME); // SDカードからデータ読み込みを実行

関数例は以下になります。

void readSdData(const char* fileName) {
  myFile = SD.open(fileName, FILE_READ);  // SDカードのファイルを開く

  if (myFile) { // ファイルが開けたら
    // データ読み込み表示
    while (myFile.available()) {  // ファイルにデータがあれば繰り返し
      char data = myFile.read();  // データを1文字づつ読み込む
      Serial.print(data);         // シリアル出力
    }
    myFile.close(); // ファイルを閉じる
  } else {  // ファイルが開けなければ
    Serial.println("ファイルが読み込めませんでした"); // シリアル出力
  }
}

読み込み処理は、まず「SD.open()」関数で「ファイル名」と「読み込みモード(FILE_READ)」を指定してファイルを開きます。(「, FILE_READ」は省略可)

ファイルが開けたら「While文」で「myFile.available()」関数を実行して、データがある間「myFile.read()」で1文字づつ読み込んで処理します。
全ての文字の読み込み処理が完了したら「myFile.close()」関数でファイルを閉じます。

・ファイル削除例

SDカードのファイルを削除するには以下の例のように「deleteSdData(“ファイル名”)」関数を、削除したい「ファイル名」を指定して実行します。

deleteSdData(SD_FILENAME) // SDカードのデータ削除を実行

関数例は以下になります。

void deleteSdData(const char* fileName) {
  if (SD.exists(fileName)) {  // ファイルが存在すれば
    // ファイルを削除
    if (SD.remove(fileName)) {  // ファイル削除を実行して成功したら
      Serial.println("ファイルを削除しました");
    } else {  // ファイル削除に失敗したら
      Serial.println("ファイルの削除に失敗しました");
    }
  } else {  // ファイルがなければ
    Serial.println("ファイルが見つかりません");
  }
}

ファイルを削除するにはまず「SD.exists(“ファイル名”)」関数でファイルが存在するかを確認します。
存在すれば「SD.remove(“ファイル名”)」関数でファイル名を指定して削除します。

・JPEG画像の読み込み例

SDカードに保存されたjpeg画像を読み込んで液晶画面に表示させるには以下の例のように、「jpegDraw(“ファイル名”)」関数をファイル名を指定して実行します。

jpegDraw(JPEG_FILENAME); // SDカードに保存されたJPEG画像のファイル名を指定して実行

関数例は以下になります。

void jpegDraw(const char* filename) {
  JpegDec.decodeSdFile(filename); // JPEGファイルをSDカードからデコード実行
  
  // シリアル出力、デコード画像情報(MCU [Minimum Coded Unit]:JPEG画像データの最小処理単位
  Serial.printf("Size : %d x %d\nMCU : %d x %d\n", JpegDec.width, JpegDec.height, JpegDec.MCUWidth, JpegDec.MCUHeight);
  Serial.printf("Components: %d\nMCU / row: %d\nMCU / col: %d\nScan type: %d\n\n", JpegDec.comps, JpegDec.MCUSPerRow, JpegDec.MCUSPerCol, JpegDec.scanType);  

  uint16_t *pImg;   // ピクセルデータ用のポインタ
  while (JpegDec.read()) {  // JPEGデータを読み込む
    pImg = JpegDec.pImage;  // 現在のピクセルデータのポインタを取得
    for (int h = 0; h < JpegDec.MCUHeight; h++) { // MCU高さ分ループ
      for (int w = 0; w < JpegDec.MCUWidth; w++) {  // MCU幅分ループ
        // 現在のピクセルのx, y座標を計算
        int x = JpegDec.MCUx * JpegDec.MCUWidth + w;  // x座標
        int y = JpegDec.MCUy * JpegDec.MCUHeight + h; // y座標
        if (x < JpegDec.width && y < JpegDec.height) {  // ピクセルが画像範囲内なら
            tft.drawRGBBitmap(x, y, pImg, 1, 1); // 液晶画面にピクセルを描画
        }
        pImg += JpegDec.comps;  // ポインタを次のピクセルデータへ進める
      }
    }
  }
}

JPEG画像を液晶画面に表示するには、デコード処理を行います。

JPEG画像のデコード処理とは、JPEG形式で圧縮された画像データを解読し、元のピクセル情報(ビットマップデータ)に変換する処理です。

デコード処理は「JpegDec.decodeSdFile(“ファイル名”)」関数をファイル名を指定して実行します。
デコード処理は「MCU(Minimaum Coded Unit)」というJPEG画像の最小処理単位で行われ、今回の動作確認では「16×16」ピクセル単位で処理されていました。

液晶画面に表示するには「MCU」の高さと幅の分「for文」でループさせて、「tft.drawRGBBitmap()」関数で1ピクセルごとに点を、指定座標に表示することで画像として表示します。

ピクセルデータを都度液晶画面に表示すると、デコード処理順に断片的に表示されていきます。
最後に紹介する「サンプルプログラム」では、ピクセルデータをメモリ内に描画することで、完成した画像を液晶画面に一括表示するようにしています。
液晶画面に画像を描画するには使用する液晶表示器ごとに、必要な初期設定、表示プログラムを実行する必要があります。

ラズパイPicoの互換ボードでSDカードリーダーを搭載した「RP2040-GEEK」を使用したmicroSDカードにデータ保存、読み込み、削除をする方法も、以下リンク先でサンプルプログラムで紹介しています。

RP2040-GEEK ラズパイPicoでSDカードと液晶使うならこれ1台でOK!
Raspberry Pi PicoにmicroSD(TF)カードリーダとTFT液晶ディスプレイ(ST7789)を搭載、RP2040-GEEKの使い方を詳しく紹介

6.サンプルプログラムで動作確認

実際にSDカードへのデータの読み書きや削除、JPEG画像データの読み込み表示行う「サンプルプログラム」を下の方に準備しましたので、動作確認してみましょう。

ここで使用するJPEG画像データは下画像のような「ラズパイPico」のイラスト(160×240ピクセル)です。

JPEG画像サンプル

以下からダウンロードできます。

JPEG画像データは事前にパソコンを使用して、SDカードに書き込んでおいてください。
※クリックしても画像が保存されない場合は「右クリック」から「名前を付けて画像を保存」を選択して保存してください。

・動作紹介

動作については以下のようになります。
「サンプルプログラム」を書き込んで実行すると、下画像のような画面が表示されます。

Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認

画面を左右2画面に分割して、左側をデータ表示、右側を操作画面として使用します。
画面の右上にはタッチ位置確認のための座標「x, y」と、電源ONからの経過時間が表示されます。

電源ONからの経過時間をダミーデータとして、SDカードに書き込み、それを読み込んだり削除することで動作確認を行います。
Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認

[Write]ボタンを押すごとに経過時間がSDカード内の「指定ファイル(test.txt)」に書き込まれます。
書き込んだデータは、左側のデータ表示画面に表示されます。

Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認

[Read]ボタンを押すと、SDカード内の「指定ファイル(test.txt)」が読み込まれて、保存されている「テキストデータ」が表示されます。

Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認

[Delete]ボタンを押すと、SDカード内の「指定ファイル(test.txt)」が削除されます。

Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認

[JPEG]ボタンを押すと、SDカード内の「JPEG画像データ(raspi_pico.jpg)」が読み込まれて画面に表示されます。

ファイルが読み込めなかったり、ファイルが存在しない等のエラー表示も下画像のように表示されるようにしています。

Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認
Raspberry Pi Pico とILI9341 SDカードリーダーの動作確認

・サンプルプログラム(コピペ)

サンプルプログラムの動作について、基本的にはここまで紹介してきた内容の応用になります。

できるだけ関数にまとめて、プログラム内にコメントを細かく書いているので、詳細はコメントを参照してください。

サンプルプログラムは以下になります。
コピペで貼り付けて書き込んでください。コピーは下の黒塗り部右上のアイコンクリックでもできます。

#include <Adafruit_GFX.h>         // Adafruitのグラフィックスライブラリ
#include <Adafruit_ILI9341.h>     // 液晶表示器 ILI9341 制御用ライブラリ
#include <XPT2046_Touchscreen.h>  // タッチパネル制御用ライブラリ
#include <JPEGDecoder.h>          // JPEG画像デコーダー用のライブラリ

#include <Fonts/FreeSans12pt7b.h> // フォントを読み込み
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans18pt7b.h> 

#define TFT_WIDTH   320 // 画面幅
#define TFT_HEIGHT  240 // 画面高さ
#define TFT_ROTATION  3 // 画面の回転(タッチパネルの回転と合わせる)

#define COMMON_SCK  18  // 液晶表示とタッチパネル、SDカードリーダー共通の SCK
#define COMMON_MOSI 19  // 液晶表示とタッチパネル、SDカードリーダー共通の MOSI
#define COMMON_MISO 16  // タッチパネル、SDカードリーダーの MISO
#define TOUCH_CS 17     // タッチパネルの CS
#define TFT_DC   20     // 液晶画面の DC
#define TFT_RST  21     // 液晶画面の RST
#define TFT_CS   22     // 液晶画面の CS

#define SD_CS 15  // SD(TF)カードリーダのCS
#define SD_FILENAME "/test.txt"          // SD(TF)カードに保存するファイル名
#define JPEG_FILENAME "/raspi_pico.jpg"  // JPEG画像ファイル名

XPT2046_Touchscreen ts(TOUCH_CS); // タッチパネルのインスタンスを作成
Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST); // ILI9341ディスプレイのインスタンスを作成
JPEGDecoder jpegDec;  // JPEGデコーダのインスタンス
File myFile;          // Fileクラスのインスタンスを宣言

// スプライト(メモリ描画領域から一括表示)をcanvas1(操作パネル画面)、canvas2(データ表示画面)として2画面準備
// 画面表示をtftではなくcanvas1,2で指定して一括描画することでチラツキなく表示できる
GFXcanvas16 canvas1(TFT_WIDTH/2, TFT_HEIGHT);  // 操作画面用スプライト(オフスクリーンバッファ)
GFXcanvas16 canvas2(TFT_WIDTH/2, TFT_HEIGHT);  // データ表示画面用スプライト(オフスクリーンバッファ)

// 変数宣言
float timeData; // SDカードに保存するダミーデータ(電源ONからの経過時間)
bool writeButtonState;  // 書き込みボタン状態格納用
bool readButtonState;   // 読み込みボタン状態格納用
bool deleteButtonState; // 削除ボタン状態格納用
bool jpegButtonState;   // Jpeg画像読み込みボタン状態格納用

/*********************** 液晶画面メッセージ表示関数 **********************/
void dispMessage(const char* text, uint16_t collor) {
  canvas2.fillScreen(collor); // 背景の塗りつぶし
  canvas2.setCursor(5, 120);  // カーソル位置
  canvas2.print(text);        // メッセージ表示
}
/************************* SDカード データ書き込み関数 ************************/
void writeSdData(const char* text) {
  // SDカードのファイルを開く
  myFile = SD.open(SD_FILENAME, FILE_WRITE);
  // データ書き込み
  if (myFile) { // ファイルが開けたら
    myFile.printf("%s : %.1f\n", text, timeData); // テキストと経過時間(秒)をファイルに書き込み
    myFile.close(); // ファイルを閉じる
    // 書き込みデータ表示
    canvas2.fillScreen(0x3103); // 背景の塗りつぶし
    canvas2.setCursor(0, 18);   // カーソル位置
    canvas2.printf("%s,%.1f\n", text, timeData); // テキストと経過時間(秒)を画面に表示
    Serial.printf("%s,%.1f\n", text, timeData);  // シリアル出力
  } else { // ファイルが開けなければ
    Serial.println("ファイルを開けませんでした");
    dispMessage("Can't open file!", ILI9341_RED); // 液晶画面メッセージ表示関数呼び出し
  }
}
/********************* SDカード データ読み込み関数 ********************/
void readSdData(const char* fileName) {
  myFile = SD.open(fileName);  // SDカードのファイルを開く
  if (myFile) { // ファイルが開けたら
    canvas2.fillScreen(0x3103); // 背景の塗りつぶし
    canvas2.setCursor(0, 18);   // カーソル位置
    // データ読み込み表示
    while (myFile.available()) {  // ファイルにデータがあれば繰り返し
      char data = myFile.read();  // データを1文字づつ読み込む
      canvas2.print(data);        // TFT出力
      Serial.print(data);         // シリアル出力
    }
    myFile.close(); // ファイルを閉じる
  } else {  // ファイルが開けなければ
    Serial.println("ファイルが読み込めませんでした");  // シリアル出力
    dispMessage("Can't read file!", ILI9341_RED); // 液晶画面メッセージ表示関数呼び出し
  }
}
/********************* SDカード データ削除関数 ********************/
void deleteSdData(const char* fileName) {
  if (SD.exists(fileName)) {  // ファイルが存在すれば
    if (SD.remove(fileName)) {  // ファイル削除を実行して成功すれば
      Serial.println("ファイルを削除しました");
      canvas2.fillScreen(0x3103);     // 背景の塗りつぶし
      canvas2.setCursor(0, 18);       // カーソル位置
      canvas2.println("Delete File"); // TFT出力
      canvas2.println(fileName);      // TFT出力
    } else {  // ファイル削除に失敗したら
      Serial.println("ファイルの削除に失敗しました");
      dispMessage("Delete Failed!", ILI9341_RED);  // 液晶画面メッセージ表示関数呼び出し
    }
  } else {  // ファイルがなければ
    Serial.println("ファイルが見つかりません");
    dispMessage("File not found!", ILI9341_RED);  // 液晶画面メッセージ表示関数呼び出し
  }
}

//********************* JPEG画像デコード処理関数 *********************//
void jpegDraw(const char* filename) {
  JpegDec.decodeSdFile(filename); // JPEGファイルをSDカードからデコード実行
  
  // シリアル出力、デコード画像情報(MCU [Minimum Coded Unit]:JPEG画像データの最小処理単位、ここでは16x16ピクセル)
  Serial.printf("Size : %d x %d\nMCU : %d x %d\n", JpegDec.width, JpegDec.height, JpegDec.MCUWidth, JpegDec.MCUHeight);
  Serial.printf("Components: %d\nMCU / row: %d\nMCU / col: %d\nScan type: %d\n\n", JpegDec.comps, JpegDec.MCUSPerRow, JpegDec.MCUSPerCol, JpegDec.scanType);  

  uint16_t *pImg;   // ピクセルデータ用のポインタ
  while (JpegDec.read()) {  // JPEGデータを読み込む
    pImg = JpegDec.pImage;  // 現在のピクセルデータのポインタを取得
    for (int h = 0; h < JpegDec.MCUHeight; h++) { // MCU高さ分ループ
      for (int w = 0; w < JpegDec.MCUWidth; w++) {  // MCU幅分ループ
        // 現在のピクセルのx, y座標を計算
        int x = JpegDec.MCUx * JpegDec.MCUWidth + w;  // x座標
        int y = JpegDec.MCUy * JpegDec.MCUHeight + h; // y座標
        if (x < JpegDec.width && y < JpegDec.height) {  // ピクセルが画像範囲内なら
            // tft.drawRGBBitmap(x, y, pImg, 1, 1); // 有効にすると液晶画面に直接描画されるのが確認できる
            canvas2.drawRGBBitmap(x, y, pImg, 1, 1); // canvas2にピクセルを描画
        }
        pImg += JpegDec.comps;  // ポインタを次のピクセルデータへ進める
      }
    }
  }
}
/******************** テキスト描画関数 ********************/
void drawText(int16_t x, int16_t y, const char* text, const GFXfont* font, uint16_t color) {
  canvas1.setFont(font);       // フォント
  canvas1.setTextColor(color); // 文字色
  canvas1.setCursor(x, y);     // 表示座標
  canvas1.println(text);       // 表示内容
}

/******************** ボタン描画関数 ********************/
void drawButton(int x, int y, const char* label, const GFXfont* font, uint16_t bgColor, uint16_t labelColor) {
  int16_t w = 150, h = 42;  // ボタン幅, 高さ
  canvas1.fillRect(x, y, w, h, ILI9341_DARKGREY);          // 外枠
  canvas1.fillRect(x + 3, y + 3, w-6, h-6, ILI9341_WHITE); // 境界線
  canvas1.fillRect(x + 6, y + 6, w-12, h-12, bgColor);     // 操作部

  // テキストの幅と高さを取得
  canvas1.setFont(font); // 表示ラベル
  int16_t textX, textY;           // テキスト位置取得用
  uint16_t textWidth, textHeight; // テキストサイズ取得用
  canvas1.getTextBounds(label, x, y, &textX, &textY, &textWidth, &textHeight);  // テキストの境界を取得する関数
  
  // 中央揃えのための新しいx, y座標の計算
  int16_t centeredX = x + (w - textWidth) / 2;               // xを中央へ
  int16_t centeredY = y + (h - textHeight) / 2 + textHeight; // yを下げて中央へ
  
  canvas1.setTextColor(labelColor);        // 文字色
  canvas1.setCursor(centeredX, centeredY); // 新しいカーソル位置を設定
  canvas1.print(label);                    // テキストを描画
}

// 初期設定 ----------------------------------------
void setup() {
  Serial.begin(115200); // シリアル通信初期化

  //SPI0設定(TFT,TOUCH,SD共通)
  SPI.setTX(COMMON_MOSI); // SPI0のTX(MOSI)
  SPI.setRX(COMMON_MISO); // SPI0のRX(MISO)
  SPI.setSCK(COMMON_SCK); // SPI0のSCK

  //液晶表示初期設定
  tft.begin();                       // TFTを初期化
  tft.setRotation(TFT_ROTATION);     // TFTの回転を設定(0-3)
  tft.setTextSize(1);                // テキストサイズ(倍率)
  canvas1.fillScreen(ILI9341_BLACK); // canvas1の背景色初期化
  canvas2.fillScreen(0x3103);        // canvas2の背景色初期化
  canvas2.setFont(&FreeSans9pt7b);   // canvas2のフォント

  //タッチパネル初期設定
  ts.begin();                   // タッチパネル初期化
  ts.setRotation(TFT_ROTATION); // タッチパネルの回転を設定(液晶画面と合わせる)

  // SDカードの初期化
  if (!SD.begin(SD_CS)) {
    Serial.println("SDカードの初期化に失敗しました");
    dispMessage("SD card not found!", ILI9341_RED);  // 液晶画面メッセージ表示関数呼び出し
    return;
  } else {
    Serial.println("SDカードが初期化されました");
  }
}

// メイン -----------------------------------------
void loop() {
  timeData = (float)millis()/1000.0;    // 経過時間を算出(秒に換算)
  canvas1.fillScreen(ILI9341_BLACK);    // 画面クリア
  //タッチパネル処理
  canvas1.setTextColor(ILI9341_WHITE);  // 文字色
  canvas1.setCursor(7, 16);             // 座標設定
  canvas1.setFont(&FreeSans9pt7b);      // フォント

  if (ts.touched() == true) {  // タッチされていれば
    TS_Point tPoint = ts.getPoint();  // タッチ座標を取得
    // タッチx座標をTFT画面の座標に換算
    int16_t x = (tPoint.x-400) * TFT_WIDTH / (4095-550);  // タッチx座標をTFT画面の座標に換算
    int16_t y = (tPoint.y-230) * TFT_HEIGHT / (4095-420); // タッチy座標をTFT画面の座標に換算
    canvas1.printf("x%d y%d", x, y);                      // タッチ座標表示

    // ボタンタッチエリア検出(各処理関数実行)
    if (x >= 177 && x <= 303 && y >= 71 && y <= 95 && !writeButtonState) {    // 書き込みボタン範囲内なら
      writeButtonState = true;    // ボタン状態をtrueへ
      writeSdData("Time(s)");     // 書き込みデータ名を指定してSDデータ書き込み関数呼び出し
    }
    if (x >= 177 && x <= 303 && y >= 115 && y <= 139 && !readButtonState) {   // 読み込みボタン範囲内なら
      readButtonState = true;     // ボタン状態をtrueへ
      readSdData(SD_FILENAME);    // ファイル名を指定してSDデータ読み出し関数呼び出し
    }
    if (x >= 177 && x <= 303 && y >= 159 && y <= 183 && !deleteButtonState) { // 削除ボタン範囲内なら
      deleteButtonState = true;   // ボタン状態をtrueへ
      deleteSdData(SD_FILENAME);  // ファイル名を指定してSDデータ削除関数呼び出し
    }
    if (x >= 177 && x <= 303 && y >= 203 && y <= 227 && !jpegButtonState){    // JPEG画像読み込みボタン範囲内なら
      jpegButtonState = true;     // ボタン状態をtrueへ
      jpegDraw(JPEG_FILENAME); // JPEG画像デコード処理関数呼び出し
    }
    // Serial.printf("x=%d, y=%d\n", x, y); // シリアル出力確認用
  } else {  //タッチされていなければ座標表示なし
    canvas1.print("x -    y -"); // 文字表示
    readButtonState = false;     // ボタン状態をfalseへ
    writeButtonState = false;
    deleteButtonState = false;
    jpegButtonState = false;
  }
  canvas1.setCursor(95, 16);         // 座標設定
  canvas1.printf("%.1fs", timeData); // 経過時間表示

  // 線描画
  canvas1.drawFastVLine(0, 0, 240, ILI9341_WHITE);  // 線(指定座標から垂線)
  canvas1.drawFastVLine(1, 0, 240, ILI9341_WHITE);  // 線(指定座標から垂線)
  canvas1.drawFastHLine(0, 24, 160, ILI9341_WHITE); // 線(指定座標から平行線)
  canvas1.drawFastHLine(0, 25, 160, ILI9341_WHITE); // 線(指定座標から平行線)
  canvas1.drawFastHLine(0, 60, 160, ILI9341_WHITE); // 線(指定座標から平行線)
  canvas1.drawFastHLine(0, 61, 160, ILI9341_WHITE); // 線(指定座標から平行線)

  // 文字描画(x, y, 内容, フォント, 文字色)
  drawText(10, 55, "SD TEST", &FreeSans18pt7b, ILI9341_ORANGE); // タイトル表示

  // ボタン描画(x, y, ラベル, フォント, ボタン色(ON/OFFで分岐), ラベル色)
  drawButton(7, 65, "Write", &FreeSans12pt7b, (writeButtonState) ? ILI9341_DARKGREY : ILI9341_BLUE, ILI9341_WHITE);     // 書き込みボタン
  drawButton(7, 109, "Read", &FreeSans12pt7b, (readButtonState) ? ILI9341_DARKGREY : ILI9341_DARKGREEN, ILI9341_WHITE); // 読み込みボタン
  drawButton(7, 153, "Delete", &FreeSans12pt7b, (deleteButtonState) ? ILI9341_DARKGREY : ILI9341_RED, ILI9341_WHITE);   // 削除ボタン
  drawButton(7, 197, "JPEG", &FreeSans12pt7b, (jpegButtonState) ? ILI9341_DARKGREY : ILI9341_PURPLE, ILI9341_WHITE);    // Jpeg画像読み込みボタン

  // スプライト(メモリ内に描画した画面)をTFTに描画
  tft.drawRGBBitmap(160, 0, canvas1.getBuffer(), TFT_WIDTH/2, TFT_HEIGHT);  // 操作パネル画面表示
  tft.drawRGBBitmap(0, 0, canvas2.getBuffer(), TFT_WIDTH/2, TFT_HEIGHT);    // データ表示エリア画面表示
}

7.まとめ

「Raspberry Pi Pico」でタッチパネル液晶表示「ILI9341」に搭載されている「SDカードリーダー」の使い方を詳しく紹介しました。

「SDカードリーダー」はSPI通信で制御され、通信に使用する端子は「液晶表示」と「タッチパネル」共通で使用でき、CS端子で切り替えて使用します。

今回使用した「ILI9341」には「SDカードリーダー/液晶表示/タッチパネル」が1枚の基板に実装されています。
通信端子を共有すれば、最小構成で10本の配線で全ての制御を行うことができるため「ラズパイPico」に限らず、多くのマイコンボードで制御することができます。

「SDカード」へのデータ読み書きや削除、JPEG画像データの読み出しは「Arduino」のライブラリを使用すれば、比較的簡単に行うことができるため、測定データを記録するデータロガーやJPEG画像を利用したディスプレイ広告、案内板等を安価に実現することができます。

タッチパネル操作画面の作成には、ボタン等部品の配置や座標の設定等に手間がかかりますが、操作パネルの写真や画像、手書きの操作画面を画像として読み込んでタッチ座標だけ設定すれば、タッチパネル式の操作画面も手軽に作成できます。

小規模な用途であれば、シーケンサとGOT(グラフィックオペレーションターミナル)を組み合わせたものと同じ機能が遥かに安価で実現でき、SDカードへのデータ記録もできるため、使いこなせば色々な用途に使用できると思います。

ラズパイPicoでタッチパネル液晶表示器 ILI9341の使い方
Raspberry Pi PicoでILI9341を使用した液晶表示とタッチパネルの使い方をArduinoIDEでお絵描きやボタン操作、値表示しながら詳しく紹介
ラズパイPicoでシーケンサを作ろう(動作確認編)基板製造、部品実装
Raspbrry Pi Picoで製作したシーケンサ(PLC)基板の動作確認(入出力、ラダー、I2C/UART通信等)を行います。基板の発注方法も詳しく紹介していきます。

コメント

タイトルとURLをコピーしました