Arduino向けILI9486 LCDパネルを使ってラズパイで動画再生(ソフト作成編その1)
前回の仕様確認編からの続きになります。
spend-carefree.hatenablog.com
今回は:
- ラズパイを使ってどのようにLCDモジュール(ILI9486)を駆動すればよいのか?
- ArduinoのLCDシールド→ラズパイ変換基板の作成
- ラズパイGPIOの性能
- LCDモジュールに画像を表示させるのに利用するコマンド
- LCDモジュールの駆動する関数
- bmp画像を表示するプログラム
を見ていきたいと思います
目次
- LCDモジュール(ILI9486)の駆動方法
- LCDモジュール <-> ラズパイ 変換基板の作成
- GPIOの性能
- LCDへの画像表示方法
- LCDモジュール駆動関数(c++)
- LCDモジュール初期化部分~1pix分だけライト(c++)
- BMP画像表示プログラム
- 参考
LCDモジュール <-> ラズパイ 変換基板の作成
LCDモジュール(Arduinoピン配列)とラズパイの間に挟む形で直結基板を作成しました。基板での配線処理が簡単になるように配線が交差しないような割り付けにしています。
ブレッドボードで配線を行っていた当初はGPIO23-GPIO16がD7-D0に対応するように作ってました。こうするとプログラムが簡潔に書けるためです。
変換基板設計図がこちら(配線見直し後)
LCDモジュールピン名 | ラズパイGPIO(BCM)番号 |
---|---|
LCD_D7 | GPIO1 |
LCD_D6 | GPIO0 |
LCD_D5 | GPIO12 |
LCD_D4 | GPIO16 |
LCD_D3 | GPIO20 |
LCD_D2 | GPIO21 |
LCD_D1 | GPIO25 |
LCD_D0 | GPIO7 |
LCD_RST | GPIO26 |
LCD_CS | GPIO19 |
LCD_RS | GPIO13 |
LCD_WR | GPIO6 |
LCD_RD | GPIO5 |
ラズパイと変換基板を合体
GPIOの性能
先ほど記載したように、LCDへアクセスするためにGPIOを使うのですが、まずはGPIOの性能を知っておきたいと思います。
これは30fpsを実現するのに十分な速度で動くのかということと、逆に、(今回Raspberry Pi 3B+を使っているのですが)コアの速度が1.4GHzと高速なのでGPIOへのアクセスが早すぎて適当なウェイトを入れないとLCDモジュール側のACスペックを満たせない懸念もあるからです。
ということで格安ロジアナ(24MHzサンプル)に登場いただくこととします。
#そもそも早すぎる懸念を払しょくするのにこの低速ロジアナの結果では役にはたたないと思うのですが、、、とりあえず見たい!って事で見る事にします。
ラズパイのGPIOの駆動の仕方
BCM2835のデータシートを確認すると、GPIOの出力値を1にするレジスタ(GPSETn)と、0にするレジスタ(GPCLRn)は別にアサインされており、各ビットがGPIO番号と対応していて1を書き込むことで出力値1,0を設定できます(0の書き込みは意味がないです)。
例えば、GPIO31-0=0x55555555を設定する場合には偶数GPIOには1,奇数GPIOには0を設定する必要があるので、GPSET[31:0]に0x55555555をGPCLR[31:0]には0xaaaaaaaaを書き込む必要があります。
つまりGPSETには書き込みたいデータそのものを、GPCLRレジスタには書き込みたいデータのビット反転を行ったデータを書き込めば実現できます。
性能測定プログラム
以上を踏まえ、GPIO14をWRX、GPIO15,GPIO18をライトデータに見立てて、性能測定プログラムを作ります。320x480pixを書き込む想定です。1pixあたり16bitデータなので2回のライトが必要です。
詳細な説明は省きますがmmapしたGPIOのレジスタのアドレスをgpset0,gpclr0で定義して、本変数に値を書き込むことでGPIOを駆動しています。また、GPIOを出力設定する部分はwiringPiのpinMode関数を利用しています。
// rootじゃないと走らない // sudo ./xxxx #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <time.h> #include <wiringPi.h> #define PI2_PERI_BASE 0x3f000000 #define PI1_PERI_BASE 0x20000000 #define PERI_BASE PI2_PERI_BASE // Raspberry 0/1 or 2/3 #define GPIO_BASE PERI_BASE + 0x00200000 #define GPIO14 14 #define GPIO15 15 #define GPIO18 18 #define WRX GPIO14 int main() { int i; char *map; volatile unsigned int *gpset0; volatile unsigned int *gpclr0; int linecount; // 1frame当たりのライン数 // GPIOをレジスタ制御するために領域確保 int g_fd = open("/dev/mem", O_RDWR | O_SYNC); if (g_fd < 0) { printf("Could not open /dev/mem fd\n"); return -1; } unsigned int* gpio_mmap = ( unsigned int*) mmap( NULL, BLOCK_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, g_fd, GPIO_BASE ); if (gpio_mmap == MAP_FAILED) { printf("Could not gpio_mmap %d\n",gpio_mmap); close(g_fd); g_fd=0; return -2; } gpset0 = (volatile unsigned int *) (gpio_mmap + 0x1c/4); gpclr0 = (volatile unsigned int *) (gpio_mmap + 0x28/4); if(wiringPiSetupGpio() == -1) return 1; // 出力設定 pinMode(GPIO14, OUTPUT); pinMode(GPIO15, OUTPUT); pinMode(GPIO18, OUTPUT); // LCD取り込み // ↓ ↓ // WRX ~~~~|____|~~~~|____|~~~~|____ // DATA X---------X---------X // X---------X---------X // LCDへ画像を流し込む想定で時間を測定する clock_t start_clock, end_clock; /* 処理開始前のクロックを取得 */ start_clock = clock(); for(i=0; i<320*480; i++){ // MSB 8bit write // WRX=0, pixdata MSB8bit送信 *gpset0 = 1<<GPIO15; *gpclr0 = (1<<GPIO14)|(1<<GPIO18); //for( int i=0; i<10; i++); // wait // WRX=1, pixdata MSB8bit送信 *gpset0 = 1<<GPIO14; //for( int i=0; i<10; i++); // wait // LSB 8bit write // WRX=0 *gpclr0 = (1<<GPIO14)|(1<<GPIO15); *gpset0 = 1<<GPIO18; //for( int i=0; i<10; i++); // wait // WRX=1 *gpset0 = 1<<GPIO14; //for( int i=0; i<10; i++); // wait } /* 処理終了後のクロックを取得 */ end_clock = clock(); /* 計測時間を表示 */ printf("clock:%f\n", (double)(end_clock - start_clock) / CLOCKS_PER_SEC);
本プログラムの実行時間は約16msとなりました。この速度なら余裕で30fpsは出そうです。
ロジアナで取得した波形
左側の画像が先ほどのプログラムを実行した波形です。右側がforループ10回のwaitをGPIOへアクセスするたびに挿入したものです(先ほどのソースコードのGPIOレジスタアクセス直後の// for行のコメントを外して波形を取得したもの)。
右側の画像が時間軸以外は期待している波形になります。WRX↑のタイミングでデータ側の信号(GPIO15,GPIO18)は安定していて、WRX↓のタイミングで変化しています。LCDドライバILI9486のデータシートによるとWRX↑のタイミングでデータを取り込むと記載されているのでこれが求める形です。
一方、左側の画像はデータ線であるGPIO15,GPIO18の変化タイミングはWRX↑だけでなくWRX↓でも変化してしまっています。これはロジアナのサンプリング周波数が遅いのでGPIOの信号レベルの変化がとらえきれてないからです。
さきほどの実験結果から320x480pix分を書き込むのに約16msでした。1回あたりのバスアクセス時間は16ms/320/480/2=52ns程度になり、その半分時間の26nsがWRX↓~WRX↑の時間という事になります。
ロジアナは24MHz=41.6ns周期なので、26nsの変化をとらえられるはずもなく。。。GPIO実験の目的である30fpsの能力があるか?については十分満たせていると言えますがACスペック的にどうか?という点についてはよくわからないという結果になりました。
波形で目視は出来ませんでしたが、先ほどのGPIOの実験結果とILI9486のデータシートに記載されているACスペック値を表にすると以下の様になりました。
パラメータ | 規格min | 実験データ | OK,NG |
---|---|---|---|
WRX間隔(WRX↓~WRX↓) | 66ns | 52ns | NG |
WRXのH幅,L幅 | 15ns | 26ns | OK |
WRX間隔はNGですが、本番のプログラムでは画像データの取得とかビット反転が必要なのでおそらくwaitを気にしなくてもスペックを満たせるだけ遅延しそうなのでとりあえずこのままとします。
LCDへの画像表示方法
LCDモジュールへコマンドを送信してレジスタ設定や表示データを書き込みます。
コマンドリストの選定
ILI9486のデータシートにはコマンド一覧が記載されています。
表示するのにどのコマンドが必要そうかピックアップします。使ったコマンドは以下になります。
- Soft Reset
- Memory Access Control
- Idle Mode OFF
- Interface Pixel Format
- Power Control 2
- Power Control 3
- VCOM Control 1
- PGAMCTRL(Positive Gamma Control)
- NGAMCTRL(Negative Gamma Control)
- Sleep OUT
- Display ON
- Memory Write (このコマンドで表示画像データを送ります)
最初からこれらすべてが分かったわけではなく、、、PowerControl系、VCOM,P/NGAMCTRLの設定は分からなかったです。特にgamma制御のPGAMCTRL,NGAMCTRLを初期値から変更しないと何も映らないので注意が必要です(実際にはうすく映っているのかもしれませんが目視出来ませんでした)。
分からない部分はarduinoのドライバのソースコードから設定値を引用
データシートにはこの辺りの記述を見つけられなかったので、以下のarduinoのドライバのソースコードを参考にしてガンマ値など分からない部分の値を決めました。
GitHub - lcdwiki/LCDWIKI_kbv: Driver for Arduino with 8Bit & 16Bit 8080 Databus
画像データの送信
表示画像データの送信にはMemory Writeコマンドを使います。本コマンドに続けるデータが表示画像データになります。送り込むデータ数(pix数)はColumn address、Page addressコマンドで設定した値で決めますが、今回は初期値のまま、つまり全ピクセルを書き換える設定で利用します。そのため、Memory Writeコマンドに続くデータ数は320x480x2=307200回(bytes)になります。
LCDモジュール駆動関数(c++)
LCDモジュールを駆動する関数を作りました。
- LCD_BUS_END(): CSXを"H"状態にします。
- LCD_COM_WRITE(): コマンドをライトします
- LCD_DATA_WRITE(): データ部をライトします
- LCD_REG_SET(): table配列に設定されたコマンド及びデータをライトします。[0]:コマンド番号、[1]データ数,[2]以降はライトデータ
#define LCD_REG_WAIT_TIME 50 #define LCD_RESX 26 #define LCD_CSX 19 #define LCD_DCX 13 #define LCD_WRX 6 #define LCD_RDX 5 #define LCD_SET_DATA(val) (\ ((val&0xc0)>>6) |\ ((val&0x20)<<7) |\ ((val&0x10)<<12) |\ ((val&0x08)<<17) |\ ((val&0x04)<<19) |\ ((val&0x02)<<24) |\ ((val&0x01)<<7) \ ) // CSX=1にする volatile void LCD_BUS_END( volatile unsigned int *gpset0) { *gpset0 = (1<<LCD_CSX ); //usleep(10); for(int i=0; i<LCD_REG_WAIT_TIME; i++); // wait } volatile void LCD_COM_WRITE( volatile unsigned int *gpset0, volatile unsigned int *gpclr0, unsigned int val ) { // CSX=0, WRX=0, COMMAND=0x2c *gpclr0 = (1<<LCD_CSX ) | (1<<LCD_WRX ) | (1<<LCD_DCX ) | LCD_SET_DATA( (~(val))&0xff ); *gpset0 = LCD_SET_DATA(val) ; // WRX=1 *gpset0 = (1<<LCD_WRX ); usleep(10); *gpset0 = (1<<LCD_DCX ); usleep(10); } volatile void LCD_DATA_WRITE( volatile unsigned int *gpset0, volatile unsigned int *gpclr0, unsigned int val ) { // WRX=0 *gpset0 = LCD_SET_DATA(val) ; *gpclr0 = (1<<LCD_WRX ) | LCD_SET_DATA( (~(val))&0xff ); usleep(10); // WRX=1 *gpset0 = (1<<LCD_WRX ); usleep(10); } volatile void LCD_REG_SET( volatile unsigned int *gpset0, volatile unsigned int *gpclr0, unsigned char *table) { unsigned int cmd; int tcnt; // 転送回数 int i; cmd = (unsigned int) *table; tcnt = (int)*(table+1); LCD_COM_WRITE(gpset0,gpclr0,cmd); if(tcnt!=0) { for(i=0; i<tcnt; i++) { LCD_DATA_WRITE(gpset0,gpclr0,*(table+2+i)); } } LCD_BUS_END(gpset0); usleep(10); }
LCDモジュール初期化部分~1pix分だけライト(c++)
1pix分だけLCDへデータを送るプログラムです。
送るデータは
(upper_data8<<8) | lower_data8
で、RGB565の1pix分になります。
Cソースではupper_data8,lower_data8が突然出てきますが、画像を送り込むイメージを記載するためにこのような記述にしています。
先のLCDモジュール駆動関数群を使っています。
// PGAMCTRL and NGAMCTRL value is: Copyright (c) 2018 lcdwiki int main() { int i; volatile unsigned int *gpset0; volatile unsigned int *gpclr0; // GPIOをレジスタ制御するために領域確保 int g_fd = open("/dev/mem", O_RDWR | O_SYNC); if (g_fd < 0) { printf("Could not open /dev/mem fd\n"); return -1; } unsigned int* gpio_mmap = ( unsigned int*) mmap( NULL, BLOCK_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, g_fd, GPIO_BASE ); if (gpio_mmap == MAP_FAILED) { printf("Could not gpio_mmap %d\n",gpio_mmap); close(g_fd); g_fd=0; return -2; } gpset0 = (volatile unsigned int *) (gpio_mmap + 0x1c/4); gpclr0 = (volatile unsigned int *) (gpio_mmap + 0x28/4); if(wiringPiSetupGpio() == -1) return 1; // 各種信号初期化 *gpset0 = (1<<LCD_RESX)| (1<<LCD_CSX) | (1<<LCD_WRX) | (1<<LCD_RDX) ; *gpclr0 = (1<<LCD_DCX); // 出力設定 pinMode(1, OUTPUT); //D7 pinMode(0, OUTPUT); //D6 pinMode(12, OUTPUT); //D5 pinMode(16, OUTPUT); //D4 pinMode(20, OUTPUT); //D3 pinMode(21, OUTPUT); //D2 pinMode(25, OUTPUT); //D1 pinMode(7, OUTPUT); //D0 pinMode(LCD_RESX, OUTPUT); pinMode(LCD_CSX, OUTPUT); pinMode(LCD_DCX, OUTPUT); pinMode(LCD_WRX, OUTPUT); pinMode(LCD_RDX, OUTPUT); usleep(100); // reset *gpclr0 = (1<<LCD_RESX); // RESETX=0 usleep(100); *gpset0 = (1<<LCD_RESX); // RESETX=1 usleep(100); unsigned char reg_tbl[256]; // LCDへの設定レジスタ int pt_reg_tbl; LCD_COM_WRITE(gpset0,gpclr0,0x01); // SoftReset LCD_BUS_END(gpset0); usleep(5000); // 5ms LCD_COM_WRITE(gpset0,gpclr0,0x36); // MAD CTL MY(b7)=1, LCD_DATA_WRITE(gpset0,gpclr0,0x80|0x08); // MY=1,MX=0, BGR 上下反転 LCD_BUS_END(gpset0); usleep(10); LCD_COM_WRITE(gpset0,gpclr0,0x38); // IDLEMODE OFF LCD_BUS_END(gpset0); usleep(10); LCD_COM_WRITE(gpset0,gpclr0,0x3a); // Interface Pixel Format=16bit/pixel LCD_DATA_WRITE(gpset0,gpclr0,0x55); LCD_BUS_END(gpset0); // Column address pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0x2a; // command reg_tbl[pt_reg_tbl++] = 4; // 回数 reg_tbl[pt_reg_tbl++] = 0x00; reg_tbl[pt_reg_tbl++] = 0x00; reg_tbl[pt_reg_tbl++] = (((320-1)&0xff00)>>8); reg_tbl[pt_reg_tbl++] = (320-1)&0xff; LCD_REG_SET(gpset0,gpclr0,reg_tbl); // Page address pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0x2b; // command reg_tbl[pt_reg_tbl++] = 4; // 回数 reg_tbl[pt_reg_tbl++] = 0x00; reg_tbl[pt_reg_tbl++] = 0x00; reg_tbl[pt_reg_tbl++] = ((480-1)&0xff00)>>8; reg_tbl[pt_reg_tbl++] = (480-1)&0xff; LCD_REG_SET(gpset0,gpclr0,reg_tbl); // Power Control 2 pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0xc1; // command reg_tbl[pt_reg_tbl++] = 1; // 回数 reg_tbl[pt_reg_tbl++] = 0x41; LCD_REG_SET(gpset0,gpclr0,reg_tbl); // Power Control 3 pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0xc2; // command reg_tbl[pt_reg_tbl++] = 1; // 回数 reg_tbl[pt_reg_tbl++] = 0x44; LCD_REG_SET(gpset0,gpclr0,reg_tbl); //VCOM Control pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0xc5; // command reg_tbl[pt_reg_tbl++] = 4; // 回数 reg_tbl[pt_reg_tbl++] = 0x00; reg_tbl[pt_reg_tbl++] = 0x91; reg_tbl[pt_reg_tbl++] = 0x80; reg_tbl[pt_reg_tbl++] = 0x00; LCD_REG_SET(gpset0,gpclr0,reg_tbl); //PGAMCTRL pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0xe0; // command reg_tbl[pt_reg_tbl++] = 15; // 回数 reg_tbl[pt_reg_tbl++] = 0x0f; reg_tbl[pt_reg_tbl++] = 0x1f; reg_tbl[pt_reg_tbl++] = 0x1c; reg_tbl[pt_reg_tbl++] = 0x0c; reg_tbl[pt_reg_tbl++] = 0x0f; reg_tbl[pt_reg_tbl++] = 0x08; reg_tbl[pt_reg_tbl++] = 0x48; reg_tbl[pt_reg_tbl++] = 0x98; reg_tbl[pt_reg_tbl++] = 0x37; reg_tbl[pt_reg_tbl++] = 0x0a; reg_tbl[pt_reg_tbl++] = 0x13; reg_tbl[pt_reg_tbl++] = 0x04; reg_tbl[pt_reg_tbl++] = 0x11; reg_tbl[pt_reg_tbl++] = 0x0d; reg_tbl[pt_reg_tbl++] = 0x00; LCD_REG_SET(gpset0,gpclr0,reg_tbl); //NGAMCTRL pt_reg_tbl=0; reg_tbl[pt_reg_tbl++] = 0xe1; // command reg_tbl[pt_reg_tbl++] = 15; // 回数 reg_tbl[pt_reg_tbl++] = 0x0f; reg_tbl[pt_reg_tbl++] = 0x32; reg_tbl[pt_reg_tbl++] = 0x2e; reg_tbl[pt_reg_tbl++] = 0x0b; reg_tbl[pt_reg_tbl++] = 0x0d; reg_tbl[pt_reg_tbl++] = 0x05; reg_tbl[pt_reg_tbl++] = 0x47; reg_tbl[pt_reg_tbl++] = 0x75; reg_tbl[pt_reg_tbl++] = 0x37; reg_tbl[pt_reg_tbl++] = 0x06; reg_tbl[pt_reg_tbl++] = 0x10; reg_tbl[pt_reg_tbl++] = 0x03; reg_tbl[pt_reg_tbl++] = 0x24; reg_tbl[pt_reg_tbl++] = 0x20; reg_tbl[pt_reg_tbl++] = 0x00; LCD_REG_SET(gpset0,gpclr0,reg_tbl); LCD_COM_WRITE(gpset0,gpclr0,0x11); // Sleepout LCD_BUS_END(gpset0); usleep(5000); // 5ms LCD_COM_WRITE(gpset0,gpclr0,0x29); // DisplayON LCD_BUS_END(gpset0); usleep(10); //LCD初期化ここまで //画像表示(以降はイメージです) LCD_COM_WRITE(gpset0,gpclr0,0x2c); // memory write reordered_gpio=LCD_SET_DATA(upper_data8); // 画像(RGB565)の上位8bit *gpset0 = reordered_gpio ; *gpclr0 = (1<<LCD_WRX ) | (~(reordered_gpio))&0x02311083; //不要なbitは0化 *gpset0 = (1<<LCD_WRX ); reordered_gpio=LCD_SET_DATA(lower_data8); // 画像(RGB565)の下位8bit *gpset0 = reordered_gpio ; *gpclr0 = (1<<LCD_WRX ) | (~(reordered_gpio))&0x02311083; *gpset0 = (1<<LCD_WRX ); }
BMP画像表示プログラム
以上を踏まえてBMP画像を表示するプログラムを示します。
BMPヘッダ解析など今回の記事とは直接関係ない部分も含んでしまっているため、長めのプログラムになっています。
コンパイル後(makeコマンドをたたいてください)、
480x320pixの24bitRGBのbmp画像を"480x320_1.bmp"というファイル名で実行dirへ置いておき、sudo ./ili9486_dispを実行します。
以下へソースコードを置きました。参考にしてください。
https://github.com/kazuokada/ili9486_disp_bmp
画像は以下になります。写真はボヤっとしてますが、実際はもっとくっきりと表示されてます。
更に次回へ続きます。長くなってしまいました。
Arduino向けILI9486 LCDパネルを使ってラズパイで動画再生(ソフト作成編その2)
spend-carefree.hatenablog.com