2016年5月12日 星期四

{ 2.8 吋 (400x240) 電阻式觸模液晶螢幕使用說明 } [ 4/4 ] - 如何讀取 MicroSD 卡中的圖片與在液晶螢幕上顯示

網頁最後修改時間:2016/6/24 

2.8 吋電阻式觸摸液晶螢幕配備有 MicroSD 卡的插槽,(影片)實測支援 MicroSD HC 16GB, Class 10 以上的記憶卡。下面將會利用這個功能,在記憶卡預先儲存六張圖片 (三張圖片,每一張圖片有兩種格式),然後讀取顯示在液晶螢幕上。

這裡的重點是對照兩種不同圖片格式的讀取對顯示速度有什麼影響 !
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
這個液晶螢幕的使用需要使用賣場改寫的函式庫與範例程式碼,不然執行時圖型、字型方向與顯示都會出現問題,即便使用原廠所提供的函式庫也是一樣 !

如果有遇到這個問題的使用者,可以參考 ILI932# 的資料手冊。雖然晶片支援的解析度不一樣,但是可以作為修改的參考,因為絕大部分的暫存器設定是相同的,只要注意一下螢幕顯示方向的部分,就可以解決大部分的問題,但要花一些時間做測試,提供給手邊有此零件的使用者做參考。

有購買商品的使用者,網頁中所需相關資料已放置於雲端硬碟,請根據網頁中所提的資訊下載相關程式碼做測試。

其餘的使用者,網頁有提供範例程式碼可供下載。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

*********************************************************************************
2.8吋(400x240)電阻式觸摸液晶螢幕可至露天賣場訂購:
*********************************************************************************

硬體環境:
  • 2.8 吋 (400x240) 電阻式觸模液晶螢幕
  • Arduino UNO
其他板子只要接腳夠與液晶螢幕接的的話,我認為都可以用 ! 但是要特別確認使用的板子接腳與 UNO 和液晶螢幕相接的號碼是否相同,不同就要改一下,這可不能懶惰不去做,可以省下很多時間的 !

軟體環境:
  • 安裝編輯環境:Arduino IDE 1.6.5-r5, Arduino IDE 1.6.7, Arduino IDE 1.6.8
  • 安裝液晶螢幕函式庫:TFTLCD-mPm
    檔案位於 {雲端硬碟}/arduino/codes/libraries
    確認 {My Documents}/Arduino/libraries 沒有其他的 TFTLCD 函式庫,不然會造成編譯時候的衝突或是出現編譯錯誤 !
    上面注意事項沒問題後,下載 TFTLCD-mPm.zip 後直接解壓縮到 {My Documents}\Arduino\libraries 目錄下,會產生一個 TFTLCD-mPm 的資料夾,裡面會包含下面幾個檔案
TFTLCD-mPm 函式庫檔案列表
  • 程式碼:tftpics.zip
    下載上面壓縮檔,或是下載 {雲端硬碟}/arduino/codes/tftpics 整個目錄,並解壓縮到 {My Documents}/Arduino/ 目錄下。目錄中有一個 bmp 的資料夾,執行這個程式需要先將資料夾裡面 *.bmp*.lcd 共六個檔案複製到 MicroSD 中,程式才能讀取和執行。
  • 圖片轉換程式:convert2lcd.zip
    解壓縮之後會有兩個目錄,分別是 linux Windows,各別代表使用的作業環境,使用方法下面會說到。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//* 截至 2016/05/09 為止,Arduino IDE ( https://www.arduino.cc/en/Main/Software ) 最新版本為 1.6.8,但不是新版本就可以用。每個程式都會將測試成功的 Arduino IDE 版本列在其中,所以若是與網頁中使用的版本不一樣而出現問題,就改用網頁建議的版本來試。

因為到現在個人認為用的最穩定的 Arduino IDE 版本是 1.6.5-r5 ! 如果之前在 Arduino IDE 寫程式編譯上傳成功,但是出現奇怪執行現象時,可以先換版本編譯再上傳試試,或許就會意料中的結果,而不是意想不到的 ! 
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

每三張照片一種格式。第一種格式圖片以黑色作為換場色;第二種格式為白色。看的時候要注意圖片顯示的方式是由上而下還是由下而上;兩種格式顯示圖片的速度是快還是慢。


使用的圖片格式說明:

我們使用的 2.8 吋電阻式觸摸液晶螢幕的解析度是 400 * 240,所以圖片大小要先將其轉換或是選擇與解析度大小一樣的圖片,才能完整顯示。

大小轉換可以使用小畫家來做。BMP 檔案像素的存放順序是從圖片最底部的像素開始存放至頂部的像素,所以稱 bottom-up。

鑒於我們所使用的圖片都是 24-bit 像素的 BMP 圖片,為了加快顯示速度,將其像素轉換為 16-bit (R5G6B6) top-down 圖片格式。

上面兩種格式顯示的效果可以對照一下影片。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 圖片格式轉換:

Bitmap (24-bit, bottom-up) 的圖片格式要轉換為 16-bit top-down 液晶螢幕使用的圖片格式,可使用上面所提供的程式。例如轉換 mlaser.bmpmlaser.lcd,指令如下:

  • Windows
    convert2lcd.exe mlaser.bmp mlaser.lcd
  • linux
    ./convert2lcd mlaser.bmp mlaser.lcd

程式碼:柿子挑軟的吃

程式碼開頭的地方宣告了三個標頭檔,除了第三個式液晶螢幕專用的函式庫,SD.h 是記憶卡的讀寫操作函式;而 SPI.h 是記憶卡使用的通訊函式庫,大部分都沒有加上所以會產生編譯錯誤,所以若自己寫或是使用其他使用到記憶卡函式庫 SD 時,記得自己加上去免得編譯時出現問題。
tftpics.ino, line 15 - 17
#include <SD.h>
#include <SPI.h>
#include "TFTLCD.h" // TFTLCD-mPm

記憶卡使用 Arduino 板子上的硬體 SPI 接線,其中啟用接腳 (CS, chip select) 設置在 <D10> (請注意 ! 跟參考電路圖不一樣) 要另外設定,這樣才能正確地讓液晶螢幕與記憶卡共用 Arduino 板子的接腳,並且能使用程式來做使用時的切換。
tftpics.ino, line 34
#define SD_CS 10     // Set the chip select line to whatever you use (10 doesnt conflict with the library)

ROTATION 的指定在前一篇的座標轉換一節已經提到跟圖片在螢幕中顯示的方向有關;圖片是橫向,USB 在右邊,所以選擇是 1 才會跟影片中顯得結果一樣。

Lx, Ly 為 setRotation(1) 的座標原點
為了加快圖片處理的速度與不佔據太多的 Arduino 記憶體空間,在下面預先定義了一塊 3 * 60 bytes 大小的暫存區來存放每一次讀取的像素資料,要改變大小就修改 BUFFPIXEL

bufferIndex 是存放像素讀取的位置,顯示 *.bmp 檔案才會用到。
tftpics.ino, line 49 - 54
#define ROTATION 1
#define BUFFPIXEL 60
#define BYTES_PER_PIXEL 3

uint8_t picBuffer[BYTES_PER_PIXEL * BUFFPIXEL];  // 3 * pixels to buffer
int bufferIndex = BYTES_PER_PIXEL * BUFFPIXEL;

*********************************************************************************
下面是兩個互補的 BMP 檔案格式說明網站,個人覺得要去看一下,避免繼續看下去後有一點艱澀
BMP點陣圖格式說明,附實例圖解
點陣圖(Bitmap)檔案格式
*********************************************************************************

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* Big Endian & Little Endian:

這個對我來說不算陌生,因為在很早之前寫 74HC595 晶片的使用說明網頁時就遇到過,簡單用個表格來看就會很清楚。當每個記憶體位置都是存放一個 byte 的時候
  • Big Endian   :記憶體位址底部放置高位元資料,所以 0x0001 放 0x87
  • Little Endian:記憶體位指底部放置低位元資料,所以 0x0001 放 0x21
data = 0x87654321
記憶體位置 Big Endian Little Endian
0x0004 0x21 0x87
0x0003 0x43 0x65
0x0002 0x65 0x43
0x0001 0x87 0x21

為什麼要說這個呢 ? 那是因為當要存放到記憶卡中的 *.bmp*.lcd 檔案時,使用 SD 函示庫取出的資料是 Little Endian 格式,而要寫到液晶螢幕中的資料必須以 Big Endian 格式輸入,所以必須做轉換。

依據每次讀取的大小,在經由下面兩個函式轉換為 Litten Endian 格式的資料。
tftpics.ino, read16(), read32(), line 235 - 257
// LITTLE ENDIAN!

uint16_t read16(File &f) {
  uint16_t d;
  uint8_t b;
  b = f.read();
  d = f.read();
  d <<= 8;
  d |= b;
  return d;
}

// LITTLE ENDIAN!
uint32_t read32(File &f) {
  uint32_t d;
  uint16_t b;
 
  b = read16(f);
  d = read16(f);
  d <<= 16;
  d |= b;
  return d;
}

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* setup():

螢幕的初始化、旋轉方向和確定 SD 卡是否可正常運行都要先在這裡設定好。程式中運行的所有訊息都可以使用 Serial Monitor 來得到;出現問題的時候先看看 !
tftpics.ino, setup(), line 56 - 73
void setup()
{
  Serial.begin(9600);
 
  tft.reset();
  tft.initDisplay(); 

  tft.setRotation(ROTATION);  

  Serial.print("Initializing SD card...");
  pinMode(10, OUTPUT);
  
  if (!SD.begin(10)) {
    Serial.println("failed!");
    while(1);
  }
  Serial.println("SD OK!");
}

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* loop():

這個函式重複循環顯示六張圖片,每張圖面顯示 5 秒,換場以全黑或是全白的畫面暫停一秒,分別以三張圖片單一種格式的方式撥放。

*.bmp 檔案使用 displayBitmap 函式負責顯示工作;*.lcd 檔案使用 bmpdraw 函式負責顯示工作。
tftpics.ino, loop(), line 259 - 301
void loop()
{
   tft.fillScreen(BLACK);
   delay(1000);
   
  // 16-bit top-down display
   displayBitmap("peacock.lcd", 0, 0);
   delay(5000);

   tft.fillScreen(BLACK);
   delay(1000);

   // ...<< 其餘省略,請自行下載程式碼 >>...

   // 24-bit bottom-up display
   bmpdraw("peacock.bmp", 0, 0);
   delay(5000);   
   
   tft.fillScreen(WHITE);
   delay(1000);
   
   // ... << 其餘省略,請自行下載程式碼 >> ...
}

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* Bitmap (24-bit, bottom-up ) 檔案讀取與處理:

Bitmap 檔案的處理需要先讀取它的檔頭資料以獲取像是寬度、長度與像素可以開始讀取的檔案偏移位址 ,... 等資訊,這些資料存放在程式開頭宣告的全局變數中
tftpics.ino, line 44 - 45
// information we extract about the bitmap file
int bmpWidth, bmpHeight;
uint8_t bmpDepth, bmpImageoffset;

Bitmap 檔頭的讀取就需要開一下我上面給的連結,對照連結中的圖解就不難了解取得的順序與資料別是什麼 (所以下面程式碼裡面的內容請自行下載參照,我移除一些讓網頁短一點,指挑重要的留下),其中 bmpImageOffset 就是像素在檔案開頭的位置,也就是要檔案的哪裡開始讀取資料。
tftpics.ino, bmpReadHeader(), line 194 - 228
boolean bmpReadHeader(File f) {
   // ...<< 其餘省略,請自行下載程式碼 >>... 
  
  bmpImageoffset = read32(f);  
  Serial.print("offset "); Serial.println(bmpImageoffset, DEC);
  
  // ...<< 其餘省略,請自行下載程式碼 >>...

  return true;
}

在未執行 bmpReadHeader 之前,bmpDraw 被呼叫後會先取得記憶卡中檔案的 file handler (sdFile),利用它讀取所需要的資料。其中最重要的就是在 bmpReadHeader 中所取得的像素偏移位址,將它輸入到 seek 函式中就可將讀取起點到這個地方來,然後就是一系列讀取像素和顯示的工作
tftpics.ino, bmpDraw(), line 133 - 148
void bmpdraw(const char *name, uint16_t x, uint16_t y) {

  File sdFile = SD.open(name);
  if( ! sdFile )
  {
    Serial.println("Error opening file.");
    return; 
  }  
  
  if( ! bmpReadHeader(sdFile) )
  {
    Serial.println("Error reading header.");
    return; 
  }
  
  sdFile.seek(bmpImageoffset);

程式特別去比較了兩種圖片格式的顯示執行速度。所以開始讀取與顯示像素資料時,先開啟計時器,並將開始時間紀錄在 time 變數中。
tftpics.ino, bmpDraw(), line 153
  uint32_t time = millis();

還記得 Bitmap 的像素資料是由底部往上存到檔案中。由於現在液晶螢幕顯示的座標原點在左上邊,因此對於 X 軸不變,Y 軸要下移動到最大值 (螢幕底部),再往上移動到最小值(螢幕頂部),依序改變螢幕上每一個點的顏色,直到 picBuffer 像素資料已經都處理完 ( >= bufferIndex ),才會再讀取一次資料直到像素資料被全部處理完。
每一個像素點被讀取出來是 8-bit 的格式,bufferIndex + 2 是 red、bufferIndex + 1 是 green、bufferIndex 則是 blue。由於寫入液晶螢幕 (writeData) 的像素數值限制為 16-bit,這三個數值必須轉換並合併為 16-bit 顏色值:red 取高位元 5-bit、green 取高位元 6-bit 和 blue 取高位元  5-bit (稱為 Color565),轉換後的 p 值就是液晶螢幕要顯示的 16-bit 顏色值。
tftpics.ino, bmpDraw(), line 160 - 180
  tft.goTo( x, y );
  for (i=0; i< bmpHeight; i++) {
    // bitmaps are stored with the BOTTOM line first so we have to move 'up'
    tft.goTo(0, bmpHeight - i - 1);

    for (j=0; j<bmpWidth; j++) {
      // read more pixels

      if (bufferIndex >= BYTES_PER_PIXEL * BUFFPIXEL) {
        tempTime =  millis();
        sdFile.read(picBuffer, BYTES_PER_PIXEL * BUFFPIXEL);
        diskTime += millis() - tempTime;
        bufferIndex= 0;
      }
      
      p = tft.Color565( picBuffer[bufferIndex+2], picBuffer[bufferIndex+1], picBuffer[bufferIndex]);
      bufferIndex += 3;

      tft.writeData(p);
    }
  }

假設取得的 RGB 三項色值為:
  • r = R(7)RRR, R(3)RRR
  • g = G(7)GGG, G(3)GGG
  • b = B(7)BBB, B(3)BBB
轉換之後
c = R(7)RRR, R(3)G(7)GG, GG(3)GB(7), BBBB(3) 
TFTLCD.cpp, Color565(), line 180 - 189
uint16_t TFTLCD::Color565(uint8_t r, uint8_t g, uint8_t b) {
  uint16_t c;
  c = r >> 3;
  c <<= 6;
  c |= g >> 2;
  c <<= 5;
  c |= b >> 3;

  return c;
}

每一次讀取記憶卡資料都會被計算時間 (tempTime) 然後加總 (diskTime),全部函式執行的時間則是完成時間減掉函式開始執行的時間  (totalTime - time)。最後關閉檔案完成 Bitmap 檔案的讀取與顯示。
tftpics.ino, bmpDraw(), line 182 - 189
 uint32_t totalTime = millis();
 Serial.print("Total time: ");
 Serial.println(totalTime - time, DEC);
 Serial.print("Disk Time: ");
 Serial.println(diskTime, DEC);
 
 sdFile.close();
}


/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 16-bit, top-down 檔案讀取與處理:

*.lcd 檔案被開啟後,在 displayBitmap 裡面 bulkWrite 會使用 loadBitmapDate 作為 callback 函式每次取回 120 bytes 資料放在 picBuffer 陣列中做處理;由於取回的是 8-bit (byte) 資料,回傳之後是要做為計算 16-bit 數量用,所以必須將取得的 8-bit 數量除以 2 才是正確要回傳的數值。
tftpics.ino, loadBitmapData(), line 78 - 83
unsigned int loadBitmapData(void *data)
{
  File *sdFile = (File *) data;
  int bytesRead = sdFile->read(picBuffer, 2 * BUFFPIXEL);
  return bytesRead / 2;
}

在未執行到 loadBitmapData 之前,displayBitmap 函式被呼叫後會先使用 SD 取得檔案的 file handler (sdFile) 作為後面取得檔案資料用。
tftpics.ino, displayBitmap(), line 94 - 102
void displayBitmap(const char *name, uint16_t x, uint16_t y)
{
  File sdFile = SD.open(name);
  
  if( ! sdFile )
  {
    Serial.println("Error opening file.");
    return; 
  }

轉換之後的檔案只留下圖片寬與高的標頭資料,是 32-bit 的格式。
tftpics.ino, displayBitmap(), line 104 - 105
  uint32_t width = read32( sdFile );
  uint32_t height = read32( sdFile );

啟動計時器,將開始時間紀錄在 time 變數。
tftpics.ino, displayBitmap(), line 110
  uint32_t time = millis();

設定繪圖區域 (整塊液晶螢幕),然後設定像素顏色開始的位址為左上邊 (top-down)。
重複讀取 *.lcd 檔案資料到像素暫存區 picBuffer,直到像素資料已全部寫入液晶螢幕並顯示,再將繪圖區域設為預設值。
tftpics.ino, displayBitmap(), line 113 - 120
  tft.setViewport(x, y, x + width - 1, y + height - 1);
  tft.goTo(y, x);
 
  int bytesRead = ( loadBitmapData(&sdFile) );
  tft.bulkWrite( (uint16_t *) picBuffer, bytesRead, loadBitmapData, &sdFile);
  
 // tft.setViewport(bx, ex, by, ey);
  tft.setDefaultViewport();

完成上面的圖形顯示工作之後,計算總共的執行時間並輸出到 Serial Monitor,最後關閉檔案。
tftpics.ino, displayBitmap(), line 122 - 126
  Serial.print(millis() - time, DEC);
  Serial.println(" ms");
  
  sdFile.close();
}

Bitmap 和 16-bit top-down 圖片格式的檔案就如上面所說的處理完成。

結論:

這四篇關於 2.8 吋電阻式觸摸液晶螢幕的使用說明到這邊已經告一段落,提供給有購買的使用者能夠更快上手,完成自己想要做的計畫。

如果手邊有這一片液晶螢幕的看倌,也可以分享一下手邊做的東西讓大家"聞香"一下 !


<<部落格相關網頁連結>>

沒有留言:

張貼留言