2019年5月12日 星期日

STM32 LCD Display


常常在手持量測工具的開發中,會使用到LCD來顯示量測資訊,如電壓等,在這邊就簡單的介紹我之前用來開發某量測工具使用LCD的控制方式!
由於並非所有STM32 MCU都有LCD顯示的功能,因此這邊選擇STM32L100C6U6A為例,假設要來點亮88.8V這樣字符的LCD88.8V需要23個點,假設選擇4COMx6SEG共有24 pixels可以滿足23個點的需求,所以會占用STM32L100C6U6A 10 pin I/O(4COM + 6SEG)LCD設計圖如下:















#define LCD_SEG0_Pin            GPIO_Pin_1
#define LCD_SEG0_GPIO_Port      GPIOA
#define LCD_SEG1_Pin            GPIO_Pin_2
#define LCD_SEG1_GPIO_Port      GPIOA
#define LCD_SEG2_Pin            GPIO_Pin_3
#define LCD_SEG2_GPIO_Port      GPIOA
#define LCD_SEG3_Pin            GPIO_Pin_6
#define LCD_SEG3_GPIO_Port      GPIOA
#define LCD_SEG4_Pin            GPIO_Pin_7
#define LCD_SEG4_GPIO_Port      GPIOA
#define LCD_SEG5_Pin            GPIO_Pin_0
#define LCD_SEG5_GPIO_Port      GPIOB    
#define LCD_COM0_Pin            GPIO_Pin_8
#define LCD_COM0_GPIO_Port      GPIOA
#define LCD_COM1_Pin            GPIO_Pin_9
#define LCD_COM1_GPIO_Port      GPIOA
#define LCD_COM2_Pin            GPIO_Pin_10
#define LCD_COM2_GPIO_Port      GPIOA   
#define LCD_COM3_Pin            GPIO_Pin_9
#define LCD_COM3_GPIO_Port      GPIOB  

void LCD_Config(void)
{
     LCD_InitTypeDef LCD_InitStructure;
     GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA | RCC_AHBPeriph_GPIOB, ENABLE);
     GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_LCD);  // com0
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_LCD);  // com1
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_LCD); // com2
     GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_LCD);  // com3
GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_LCD);  // seg0
GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_LCD);  // seg1       
GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_LCD);  // seg2
GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_LCD);  // seg3
GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_LCD);  // seg4
GPIO_PinAFConfig(GPIOB, GPIO_PinSource0, GPIO_AF_LCD);  // seg5
     GPIO_InitStructure.GPIO_Pin = LCD_SEG0_Pin | LCD_SEG1_Pin |   
     LCD_SEG2_Pin | LCD_SEG3_Pin | LCD_SEG4_Pin | LCD_COM0_Pin |  
     LCD_COM1_Pin | LCD_COM2_Pin;                                                                                                     
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
     GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
     GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
     GPIO_Init(GPIOA, &GPIO_InitStructure);
     GPIO_InitStructure.GPIO_Pin = LCD_SEG5_Pin | LCD_COM3_Pin;
     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
     GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
     GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
     GPIO_Init(GPIOB, &GPIO_InitStructure);    
  /*!< Configure the LCD interface */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_LCD, ENABLE);
  LCD_BlinkConfig(LCD_BlinkMode_Off, LCD_BlinkFrequency_Div8);
  LCD_InitStructure.LCD_Prescaler = LCD_Prescaler_4;
  LCD_InitStructure.LCD_Divider = LCD_Divider_16;
  LCD_InitStructure.LCD_Duty = LCD_Duty_1_4;
}

由於一個com會對應到44 seg,一個comseg共需要2個32 bit(S00~S43,其餘空白)的大小來存放顯示資料,但LCD圖面只用到4COMx6SEG,因此只須宣告一個32bit變數來對應S00~S31,由於使用了4COM所以必須宣告四個變數如下:
uint32_t LCD_COM0_SEGx = 0;
uint32_t LCD_COM1_SEGx = 0;
uint32_t LCD_COM2_SEGx = 0;
uint32_t LCD_COM3_SEGx = 0;
LCD register map and reset values
















#define COMx_S00        0x00000001 << 0
#define COMx_S01        0X00000001 << 1  
#define COMx_S02        0X00000001 << 2 
#define COMx_S03        0X00000001 << 3 
#define COMx_S04        0X00000001 << 4 
#define COMx_S05        0X00000001 << 5 
#define COM0_S0_S31     0       // COM0-S0~S31
#define COM1_S0_S31     2       // COM1-S0~S31
#define COM2_S0_S31     4       // COM2-S0~S31
#define COM3_S0_S31     6       // COM3-S0~S31
enum
{
     CHAR_0 = 0,
     CHAR_1 = 1,
     CHAR_2 = 2,
     CHAR_3 = 3,
     CHAR_4 = 4,
     CHAR_5 = 5,
     CHAR_6 = 6,
     CHAR_7 = 7,
     CHAR_8 = 8,
     CHAR_9 = 9,
     CHAR_A = 10,
     CHAR_a = 10, 
     CHAR_B = 11,
     CHAR_b = 11,
     CHAR_C = 12,
     CHAR_c = 12,
     CHAR_D = 13,
     CHAR_d = 13,
     CHAR_E = 14,
     CHAR_e = 14,
     CHAR_F = 15,
     CHAR_f = 15,
     CHAR_H = 16,
     CHAR_h = 16,
     CHAR_I = 17,
     CHAR_i = 17,
     CHAR_J = 18,
     CHAR_j = 18,
     CHAR_L = 19,
     CHAR_l = 19,
     CHAR_N = 20,
     CHAR_n = 20,
     CHAR_O = 21,
     CHAR_o = 21,
     CHAR_P = 22,
     CHAR_p = 22,
     CHAR_R = 23,
     CHAR_r = 23,
     CHAR_S = 24,
     CHAR_s = 24,
     CHAR_T = 25,
     CHAR_t = 25,
     CHAR_U = 26,
     CHAR_u = 26,
     CHAR_Y = 27,
     CHAR_y = 27,
     CHAR_DESH = 28,
     CHAR_NULL = 29
};
#define DIG1 0
#define DIG2 1
#define DIG3 2
#define DIG4 3
另外我會撰寫LCDTABLE如下:
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, b, C, d, E, F, H, I, J, L, n, O, P, r, S, t, U, Y, -, NULL
const uint8_t LCD_Digit_Table[30] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F,0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x76, 0x06, 0x0E, 0x38, 0x54, 0x3F, 0x73, 0x50, 0x6D, 0x78, 0X3e, 0x6E, 0x40, 0x00};
由於TABLE屬於常數資料,因此需宣告為const另外再寫一個用來顯示控制的七結構變數
union LCD_Digit
{
    uint8_t Byte;
    struct
    {
        unsigned a : 1;
        unsigned b : 1;
        unsigned c : 1;
        unsigned d : 1;
        unsigned e : 1;
        unsigned f : 1;
        unsigned g : 1;
        unsigned p_v : 1;
    };
}Dig[3] = {0, 0, 0};

void LCD_COM0_SEGx_Update(void)
{
    LCD_COM0_SEGx = 0;
    if(Dig[0].d)
        LCD_COM0_SEGx |= COMx_S00;   // COM0-SEG0
    if(Dig[2].d)
        LCD_COM0_SEGx |= COMx_S02;   // COM0-SEG2
    if(Dig[3].p_v)
        LCD_COM0_SEGx |= COMx_S03;   // COM0-SEG3
    if(Dig[4].d)
        LCD_COM0_SEGx |= COMx_S04;   // COM0-SEG4
    if(Dig[5].p_v)
        LCD_COM0_SEGx |= COMx_S05;   // COM0-SEG5 
    return;
}

void LCD_COM1_SEGx_Update(void)
{
    LCD_COM0_SEGx = 0;
    if(Dig[0].e)
        LCD_COM0_SEGx |= COMx_S00;   // COM1-SEG0
    if(Dig[0].c)
        LCD_COM0_SEGx |= COMx_S01;   // COM1-SEG1
    if(Dig[1].e)
        LCD_COM0_SEGx |= COMx_S02;   // COM1-SEG2
    if(Dig[1].c)
        LCD_COM0_SEGx |= COMx_S03;   // COM1-SEG3
    if(Dig[2].e)
        LCD_COM0_SEGx |= COMx_S04;   // COM1-SEG4
    if(Dig[2].c)
        LCD_COM0_SEGx |= COMx_S05;   // COM1-SEG5 
    return;
}

void LCD_COM2_SEGx_Update(void)
{
    LCD_COM0_SEGx = 0;
    if(Dig[0].g)
        LCD_COM0_SEGx |= COMx_S00;   // COM2-SEG0
    if(Dig[0].b)
        LCD_COM0_SEGx |= COMx_S01;   // COM2-SEG1
    if(Dig[1].g)
        LCD_COM0_SEGx |= COMx_S02;   // COM2-SEG2
    if(Dig[1].b)
        LCD_COM0_SEGx |= COMx_S03;   // COM2-SEG3
    if(Dig[2].g)
        LCD_COM0_SEGx |= COMx_S04;   // COM2-SEG4
    if(Dig[2].b)
        LCD_COM0_SEGx |= COMx_S05;   // COM2-SEG5 
    return;
}

void LCD_COM3_SEGx_Update(void)
{
    LCD_COM0_SEGx = 0;
    if(Dig[0].f)
        LCD_COM0_SEGx |= COMx_S00;   // COM3-SEG0
    if(Dig[0].a)
        LCD_COM0_SEGx |= COMx_S01;   // COM3-SEG1
    if(Dig[1].f)
        LCD_COM0_SEGx |= COMx_S02;   // COM3-SEG2
    if(Dig[1].a)
        LCD_COM0_SEGx |= COMx_S03;   // COM3-SEG3
    if(Dig[2].f)
        LCD_COM0_SEGx |= COMx_S04;   // COM3-SEG4
    if(Dig[2].a)
        LCD_COM0_SEGx |= COMx_S05;   // COM3-SEG5 
    return;
}


void LCD_Digit_Display(uint8_t Digit, const unsigned char Table)
{  
    switch(Digit)
    { 
        case 0 :  Dig[0].Byte = Table; break;
        case 1 :  Dig[1].Byte = Table; break;
        case 2 :  Dig[2].Byte = Table; break;
        case 3 :  Dig[3].Byte = Table; break;
        default : break;   
    }
    return;

}

void LCD_Char_Display(const uint8_t *ptr)
{
     uint8_t cnt = 0;
     uint8_t LCD_Digit[4] = {CHAR_NULL, CHAR_NULL, CHAR_NULL, CHAR_NULL};
     while(*ptr != 0x00)
     {
          switch(*ptr)
          {
              case '0':       
                   LCD_Digit[cnt] = CHAR_0; break;
              case '1':
                   LCD_Digit[cnt] = CHAR_1; break;                          
              case '2':
                   LCD_Digit[cnt] = CHAR_2; break;
              case '3':
                   LCD_Digit[cnt] = CHAR_3; break;                                        
              case '4':
                   LCD_Digit[cnt] = CHAR_4; break;
              case '5':
                   LCD_Digit[cnt] = CHAR_5; break;                          
              case '6':    
                   LCD_Digit[cnt] = CHAR_6; break;
              case '7':
                   LCD_Digit[cnt] = CHAR_7; break;                               
              case '8':
                   LCD_Digit[cnt] = CHAR_8; break;
              case '9':
                   LCD_Digit[cnt] = CHAR_9; break;                               
              case 'A':
                   case 'a': LCD_Digit[cnt] = CHAR_A; break;
              case 'B':
                   case 'b': LCD_Digit[cnt] = CHAR_B; break;
              case 'C':
                   case 'c': LCD_Digit[cnt] = CHAR_C; break;
              case 'D':
                   case 'd': LCD_Digit[cnt] = CHAR_D; break;
              case 'E':
                   case 'e': LCD_Digit[cnt] = CHAR_E; break;
              case 'F':
                   case 'f': LCD_Digit[cnt] = CHAR_F; break;           
              case 'H':
                   case 'h': LCD_Digit[cnt] = CHAR_H; break;                                   
              case 'I':
                   case 'i': LCD_Digit[cnt] = CHAR_I; break; 
              case 'J':
                   case 'j': LCD_Digit[cnt] = CHAR_J; break; 
              case 'L':
                   case 'l': LCD_Digit[cnt] = CHAR_L; break;                           
              case 'N':
                   case 'n': LCD_Digit[cnt] = CHAR_N; break;                           
              case 'O':
                   case 'o': LCD_Digit[cnt] = CHAR_O; break;
              case 'P':
                   case 'p': LCD_Digit[cnt] = CHAR_P; break;                           
              case 'R':
                   case 'r': LCD_Digit[cnt] = CHAR_R; break;
              case 'S':
                   case 's': LCD_Digit[cnt] = CHAR_S; break;                           
              case 'T':
                   case 't': LCD_Digit[cnt] = CHAR_T; break;
              case 'U':
                   case 'u': LCD_Digit[cnt] = CHAR_U; break;
              case 'Y':
                   case 'y': LCD_Digit[cnt] = CHAR_Y; break;
              case '-': LCD_Digit[cnt] = CHAR_DESH;  break;
              default : LCD_Digit[cnt] = CHAR_NULL;  break;
          }
          ptr++;
          cnt++;
     }
     LCD_Digit_Display(DIG1, LCD_Digit_Table[LCD_Digit[0]]);
     LCD_Digit_Display(DIG2, LCD_Digit_Table[LCD_Digit[1]]);
     LCD_Digit_Display(DIG3, LCD_Digit_Table[LCD_Digit[2]]);
     LCD_Digit_Display(DIG4, LCD_Digit_Table[LCD_Digit[3]]);  
     }
}

void LCD_Integer_Display(int16_t Number)
{
    uint8_t LCD_Digit[3] = {CHAR_NULL, CHAR_NULL, CHAR_NULL, CHAR_NULL};
   
    LCD_Digit[0] = Number / 100;
    LCD_Digit[1] = (Number / 10) % 10;
    LCD_Digit[2] = Number  % 10;
   
    if(LCD_Digit[0] == 0)
    {
        LCD_Digit[0] = CHAR_NULL;
    }
    else
        LeadZero = 0;

     LCD_Digit_Display(DIG1, LCD_Digit_Table[LCD_Digit[0]]);
     LCD_Digit_Display(DIG2, LCD_Digit_Table[LCD_Digit[1]]);
     LCD_Digit_Display(DIG3, LCD_Digit_Table[LCD_Digit[2]]);
     LCD_Digit_Display(DIG4, LCD_Digit_Table[LCD_Digit[3]]);  
}

void LCD_Handler(void)
{
    LCD_COM0_SEGx_Update();
    LCD_COM1_SEGx_Update();
    LCD_COM2_SEGx_Update();
    LCD_COM3_SEGx_Update();
                      
    LCD_Write(LCD_RAMRegister_0, LCD_COM0_SEGx);     // COM0SEG0~COM0SEG43
    LCD_Write(LCD_RAMRegister_2, LCD_COM1_SEGx);     // COM1SEG0~COM1SEG43
    LCD_Write(LCD_RAMRegister_4, LCD_COM2_SEGx);     // COM2SEG0~COM2SEG43
    LCD_Write(LCD_RAMRegister_6, LCD_COM3_SEGx);     // COM3SEG0~COM3SEG43
    LCD_UpdateDisplayRequest();
 
  /* Wait until the update display is finished */
  while(LCD_GetFlagStatus(LCD_FLAG_UDD) == RESET)
  {}
}

假設要顯示12.0V請先將12.0放大10倍成120然後套用以下程式
LCD_Integer_Display(120);
Dig[DIG2].p_v = 1; // “.”
Dig[DIG3].p_v = 1; // “V”









2019年5月9日 星期四

Intel 8080-like and Motorola 6800-like interfaces的LCD控制介面

近日由於要開發某測試器,使用COG LCD display,因此寫個筆記,後續自己用STM32寫driver來控制吧!

目前選用的COG LCD使用Sitronix的ST7565P控制器,有三個控制LCD的介面可以讓MCU來讀寫,分別為6800/8080/SPI-4,由於SPI-4為串列介面,必須為8bit依序傳送,雖然LCD更新顯示不需要太快,但拖太久也不好,假設SPI跑1MHz,猜測送完1byte至少花10us以上,以我選擇的COG LCD為128X64,總共有8 pages的顯示資料,每個page有128 bytes,總共需要1024 bytes,若使用SPI更新全畫面需要10ms,基於耗電考量,改使用Parallel介面來控制,速度就快多了!

在COG或其它點矩陣的LCD常用介面為6800 or 8080介面,簡單來說就是8bit(D7~D0)資料寬度的並列介面,並用3個控制RS. E. R/W(6800)或RS. RD. RW(8080)來控制讀寫資料或讀寫Instruction,
下圖為參考ST AN2790 Table 2的資料,

ST7565P datasheet裡面有些不一樣的地方是RS pin命名為A0, A0 = 0 for instruction read or wirte; A0 = 1 for data read or write,其他兩個控制pin分別為RD(E)及RW(R/W),兩個pin讓6800與8080共用,選擇6800則使用E(Enable = 1)及RW(Read = 1, Write = 0);選擇8080則使用RD及RW,兩者皆為Low active
簡單來說6800與8080之間的差異在於控制這兩pin的方式, 6800使用E(Enable)及R/W控制,讀寫時E須為1,且以R/W來區分Read(1) or Write(0); 而8080則使用RD及RW控制讀或寫,但沒有Enable控制,請注意觀察上表的控制pin可能會有上標線,代表是Low active,由於這邊文字無法打成上標線,因此須注意一下!

以我本人來說,覺得6800比較直觀,用R/W控制讀或寫,且用Enable控制是否要寫入
// 6800 Interface
void Write_data(unsigned char data)
{
         LCM_RS = 1;              // 將寫入LCD DDRAM
         LCM_RW = 0;             // Writes display data ram 
         LCM_PORT=data;     // 將資料寫入8bit寬度的bus
         LCM_ENABLE = 1;    // Enable write
         _nop_();                       // wait for lcd controller data latch
         LCM_ENABLE = 0;    // Disable write
}

void Write_command(unsigned char command)
{
       LCM_RS = 0;                     // 將寫入instruction register
       LCM_RW = 0;                    // Writes instruction register
       LCM_PORT = command;   // 將command寫入8bit寬度的bus
       LCM_ENABLE = 1;          // Enable write
       _nop_();                             // wait for lcd controller data latch
       LCM_ENABLE = 0;          // Disable write
}

上面兩個副程式分別用來寫入DDRAM或Instruction,只要有這兩個基本概念,再將兩個副程式用來應用設計就可以成為initialize LCD的程式及寫入圖形的程式,LCD的dot matrix,就可以顯示圖形了!



二維陣列指標的表示方法