PICでGLCDを駆動

以前Aliで何かのついでにポチッた128x128 262KColor SPI GLCD(グラフィック液晶)(たしか@350円ほど)が眠っていたので、駆動させてみました。
ATmega系のサンプルは多数サイトにアップされているので、とりあえずATmega168で動かせて動作を確認してみました。
コントローラはST7735って書いてあったけど、ILI9163のライブラリで動きました。( <- コンパチか?)
I/FはSPIって事ですが、2線式シリアル(データとクロック)なので、タイミングさえ合えばSPIのMasterModeでもUSART in SPI mode(ちょっと遅いですが)でも駆動できます。


図1.ATmega168で組んでみた温湿度計
(USART in SPI modeで駆動中)
USART in SPI modeは本来のSPIを他の外付け機器で占有されてしまっている場合に使うと便利です。

上記はAVRでの話ですが、ここから本題です。
Webサイトでも、PICで駆動させる情報が少なかったので、覚え書き程度に書いておきます。
なお、使用PICは16F886です。3種類の転送方法を試すため、PORTCで駆動しています。(コンパイラはXC8 <-もちろんフリー版)

宣言部およびメイン

プログラムは、単純にLCDの画面を周期的(1秒おき)にRGBWで塗りつぶすだけのプログラムです。
LCDの初期化は簡略化してあるので、ガンマ補正とかは使用していません。
LCDとの通信部分(spi_init、spi_send)の3種類の転送方法は、別途記述します。

#include <xc.h>
#include <pic16f886.h>

#ifndef _XTAL_FREQ
#define _XTAL_FREQ	4000000
#endif

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = ON       // Power-up Timer Enable bit (PWRT enabled)
#pragma config MCLRE = OFF       // RE3/MCLR pin function select bit (RE3/MCLR pin function is RE3)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = OFF       // Internal External Switchover bit (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is disabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR21V   // Brown-out Reset Selection bit (Brown-out Reset set to 2.1V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

// data type definition
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned long uint32_t;

// I/F pin definition
#define _CS     RC0
#define _RESET  RC1
#define _A0     RC2
#define _SDA    RC5
#define _SCK    RC3
// LCD size definition
#define LCD_HSIZE   0x80
#define LCD_VSIZE   0x80
#define LCD_HPIX    (LCD_HSIZE-1)
#define LCD_VPIX    (LCD_VSIZE-1)
// Color definition for RGB=565(16bit)
#define COLOR_BLUE      0xF800
#define COLOR_RED       0x001F
#define COLOR_GREEN     0x07E0
#define COLOR_WHITE     0xFFFF

typedef enum {
    DATA = 1,
    COMMAND = 0
} ParamType;

void spi_init(void);
void spi_send(uint8_t data);
void writeByte(uint8_t address, ParamType dc);
void writeWord(uint16_t value);
void writePixel(uint16_t value, short size);
void ILI9163_init(void);

void writeByte(uint8_t value, ParamType dc) {
    _CS = 0;
    _A0 = (unsigned)dc;
    // send data by SPI
    spi_send(value);
    _CS = 1;
}

void writeWord(uint16_t value) {
    _CS = 0;
    _A0 = (unsigned)DATA;
    // send data by SPI
    spi_send((uint8_t)((value >> 8) & 0x00FF));
    spi_send((uint8_t)(value & 0x00FF));
    _CS = 1;
}

void writePixel(uint16_t value, short size) {
    _CS = 0;
    _A0 = (unsigned)DATA;
    // send data by SPI
    for (short i=0;i<size;i++) {
        spi_send((uint8_t)((value >> 8) & 0x00FF));
        spi_send((uint8_t)(value & 0x00FF));
    }
    _CS = 1;
}

// Initialise the display with the require screen orientation
void ILI9163_init(void) {
    _CS = 1;
    
    // Hardware reset the LCD
    _RESET = 0;
    __delay_ms(50);
    _RESET = 1;
    __delay_ms(120);
    
    writeByte(0x11, COMMAND); // Exit sleep mode command
    __delay_ms(120); // Wait for voltage stable to wake up
    
    writeByte(0x3A, COMMAND); // Set color format command
    writeByte(0x55, DATA); // 16 bits per pixel
   
    writeByte(0x29, COMMAND); // Set display on command
}

void ILI9163_fill(uint16_t color) {
    
    writeByte(0x2A, COMMAND);   // Set column address command
    writeWord(0);               // Start = 0
    writeWord(LCD_HPIX);        // End = LCD horizontal size -1 

    writeByte(0x2B, COMMAND);   // Set page address command
    writeWord(0);               // Start = 0
    writeWord(LCD_VPIX);        // End = LCD vertical size -1
    
    writeByte(0x2C, COMMAND); // Write memory command
    writePixel(color, LCD_HSIZE*LCD_VSIZE); // fill LCD size by color data
}


void main(void) {
    
    uint16_t colr[] = {COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_WHITE};
    OSCCON = 0b01101000 ;  // Internal 4MHz(default value)
    TRISA = 0b00000000;    // Set output for non use port
    TRISB = 0b00000000;
    
    spi_init();             // TRISC & PORTC & SPI setting
    ILI9163_init();         // Initialize LCD module
    
    // Loop for change color LCD
    for (char i=0; ; i++) {
        ILI9163_fill(colr[i % 4]);
        __delay_ms(1000);
    }
}

GPIO駆動

まずはオーソドックスなPIO駆動です。ILI9163のタイミングチャートさえ読めば簡単に駆動できます。

LCD Pin PIC Pin
-CS RC0
-RESET RC1
A0 RC2
SDA RC5
SCK RC3
void spi_init(void) {
    PORTC   = 0b11111111;
    TRISC   = 0b00000000;
    _SCK = 0;
}
void spi_send(uint8_t data) {
    for(char i=0;i<8;i++) {
        if(data & 0x80) _SDA = 1; else _SDA = 0;
        _SCK = 1;
        data <<= 1;
        _SCK = 0;
    }
}

やはりPIO転送は遅いです。(動作確認以外に使う事は無いと思います)

MASTER SYNCHRONOUS SERIAL PORT (MSSP)駆動

次にシリアル専用モジュールのMSSP駆動です。PICのマニュアルを読めば簡単に駆動できるかと・・ただ注意すべきはクロックの極性です(CKP=0,CKE=1)。
後は、データの送信完了を待つ事です。(SSPBUFにデータを代入して放置する人もWebサイトでは散見されますが)

LCD Pin PIC Pin
-CS RC0
-RESET RC1
A0 RC2
SDA RC5(SDO)
SCK RC3(SCK)
void spi_init(void) {
    PORTC   = 0b11111111;
    TRISC   = 0b00000000;
    _SCK = 0;
    SSPCON  = 0b00100000; // SSPEN=1, CKP=0, SSPM=0x00(SPI Master mode, clock = FOSC/4)
    SSPSTAT = 0b01000000; // CKE=1
}

void spi_send(uint8_t data) {
    SSPBUF = data;      // data output to SPI buffer
    while (SSPIF == 0); // wait until the transmission is finished
    SSPIF = 0;          // clear SSPIF
}

さすがに速いです。でも同一クロックではAVRの方が速い気がします。

ENHANCED UNIVERSAL SYNCHRONOUS ASYNCHRONOUS RECEIVER TRANSMITTER (EUSART)駆動

長いモジュール名ですね(^^;)。あまり、このモードでSPI通信をする人が少ないようですが、一応動いたので掲載します。
EUSARTは、SYNCHRONOUS MASTER MODEで使用します。
このモードで使う場合も、MSSPと同様にクロックの極性に注意する必要がありますが、一番の問題は、LSB Firstでデータが出力されてしまう点です。
よって、データを出力する前にLSB-MSBの上位・下位ビット入れ替えを行う必要があります。

LCD Pin PIC Pin
-CS RC0
-RESET RC1
A0 RC2
SDA RC7(DT)
SCK RC6(CK)
void spi_init(void) {
    PORTC   = 0b11111111;
    TRISC   = 0b00000000;
    _SCK = 0;
    SYNC = 1;    // Synchronous mode
    BRG16 = 0;   // not use 16bit baud rate register
    SPBRG = 15;  // baud rate i.e.(fosc / (15 + 1)
    SCKP = 1;    // Synchronous Clock Polarity
    SPEN = 1;    // Serial Port Enable
    CSRC = 1;    // Clock Source = baud rate gen.
    CREN = 0;    // Continuous Receive disable
    SREN = 0;    // Single Receive disable
}

void spi_send(uint8_t data) {
    uint8_t txd = 0;
    for (char i=0;i<8;i++) {    // bit swap msb-lsb
        txd <<=1;
        txd |= (data & 1);
        data >>= 1;
    }
    TXEN = 1;
    TXREG = txd;
    while (TRMT == 0) ;
    TXEN = 0;
}

SPBRGはボーレートです。小さくすると高速になりますが、これ以上小さくしても速くなりません。
やはり、ビット上位・下位の入れ替えに時間がかかっているようです。
(この辺は、AVRのUSART in SPI modeの方が優れていますね。<- MSB Firstが選択できるので)
このモードも、MSSPが他の機器用に占有されている場合の代替手段の用途かと・・

【補足】UNIVERSAL SYNCHRONOUS ASYNCHRONOUS RECEIVER TRANSMITTER (USART)駆動


図2.クロック反転回路
(TrまたはHC04等のインバータでも可)

EUSARTで動いたので、ついでにUSARTしか持っていないデバイス(今回はPIC16F648)で試してみます。
USARTは、EUSARTと違って、クロックの極性の反転ができません。よってハードを少々改良してSCKラインを反転させる必要があります。(図2)
クロック反転はどんな手段でも良いのですが、MOS-FETによる回路が一番シンプルです。ただ、Vcc=3Vで駆動している場合はFETのVGS(th)が小さいものを使う必要があります。 2N7002のVGS(th)は、2V(Typ)なので、PICの出力VOH(VDD-0.7V)=2.3Vだとぎりぎりですが...
ロジックインバーターやNAND、NORゲートが余っている場合は、それを使う方がもっとシンプルです。

LCD Pin PIC Pin
(PIC16F648)
-CS RB3
-RESET RB4
A0 RB5
SDA RB1(DT)
SCK RB2(CK)
// I/F pin definition for PIC16F648
#define _CS     RB3
#define _RESET  RB4
#define _A0     RB5
#define _SDA    RB1
#define _SCK    RB2

void spi_init(void) {
    PORTB   = 0b11111111;
    TRISB   = 0b00000000;
    _SCK = 1;
    SYNC = 1;    // Synchronous mode
    SPBRG = 3;   // baud rate i.e.(fosc / (3 + 1)
    SPEN = 1;    // Serial Port Enable
    CSRC = 1;    // Clock Source = baud rate gen.
    CREN = 0;    // Continuous Receive disable
    SREN = 0;    // Single Receive disable
}

void spi_send(uint8_t data) {
    uint8_t txd, ord = data;
    char cnt;
    
    cnt = 8;
    txd = 0;
    asm("bitloop: rrf spi_send@ord,f");
    asm("rlf spi_send@txd, f");
    asm("decfsz spi_send@cnt, f");
    asm("goto bitloop");
    TXEN = 1;
    TXREG = txd;
    while (TRMT == 0) ;
    TXEN = 0;
}

今回はビット入れ替えにインラインアセンブラを使用しました。
Fosc=4MHzで、SPBRG=3、つまり4MHz/(4*(3+1))=250Kbpsまでボーレートを上げられました。
関数のパラメータ(data)は一旦ローカル変数(ord)に代入してそれを使わないと、変更(今回の場合、右シフトrrf)が効かないみたいです。
EUSARTもこの方法で速くなると思います。
(spi_send自体をすべてアセンブラにすればもっと速くなりそうですが・・)