Skip to content

ST7789芯片的LCD驱动

这里的驱动我们使用的是由st7789芯片驱动的LCD为例. 由于st7789的外接接口是SPI, 所以我们使用的是SPI驱动.
具体的使用方法可以参考:
ESP32-C3的SPI驱动

ST7789的初始化

因为是 st7789 是一个通用的芯片,它可以适配各种尺寸和各种设计的屏幕,所以它的初始化需要根据具体的屏幕来进行调整。也就是你买屏幕的时候,需要找厂家要的初始化代码。我们要使用厂家给的初始化代码来初始化屏幕。不能自己在网上随便找一个程序就用来初始化,否则会出现问题。

第1步:单片机各引脚的初始化

这一步主要是做一些与st7789芯片相关的引脚的初始化。这个也st7789芯片没有什么关系。主要是SPI接口的初始化和DC\RST\CS引脚的初始化。以及要用到的接口SPI的初始化。让SPI可以正常工作。 一般st7789芯片的LCD提供有以下几个引脚:

  • DC引脚:数据/命令选择引脚,用于区分是数据还是命令。
  • RST引脚:复位引脚,用于复位st7789芯片。
  • CS引脚:片选引脚,用于选择st7789芯片。
  • SCK引脚:时钟引脚,用于与st7789芯片通信。
  • MOSI引脚:数据引脚,用于与st7789芯片通信。
  • MISO引脚:数据引脚,用于与st7789芯片通信。(这个引脚也可以不接
  • BL引脚:背光控制引脚,用于控制背光的亮度。(不同屏幕的背光控制引脚可能不同,可能取1,也可能取0,可以通过测试获知) 根据具体的屏幕,我们需要根据屏幕的规格来初始化这些引脚。比如屏幕的分辨率、屏幕的方向、屏幕的亮度等等。

一般情况下: MISO引脚是不接的。不接的话,有个坏处,就是不能接收来自st7789芯片的数据。比如我们要检测芯片的类型,或者要检测芯片的状态等等。就不能操作了。

第2步:厂家给的初始化代码

根据第一步各引脚的初始化后,我们使用SPI接口给st7789芯片发送厂家给的初始化代码。其实就是写入st7789芯片相应的寄存器的值。

stm32的屏幕初始化代码

c
//************* Start Initial Sequence **********//
	LCD_WR_REG(0x11); //退出休眠模式
	delay_ms(120);    // 延时120ms
	//************* Start Initial Sequence **********// 
	
    LCD_WR_REG(0x36); //这是写入的命令,0x36是寄存器地址
    //根据屏幕方向设置,不同的值
	if(USE_HORIZONTAL==0)LCD_WR_DATA8(0x00);
	else if(USE_HORIZONTAL==1)LCD_WR_DATA8(0xC0);
	else if(USE_HORIZONTAL==2)LCD_WR_DATA8(0x70);
	else LCD_WR_DATA8(0xA0);

	LCD_WR_REG(0x3A);	//命令寄存器		
	LCD_WR_DATA8(0x05);

	LCD_WR_REG(0xB2);			
	LCD_WR_DATA8(0x0C);
	LCD_WR_DATA8(0x0C); 
	LCD_WR_DATA8(0x00); 
	LCD_WR_DATA8(0x33); 
	LCD_WR_DATA8(0x33); 			

	LCD_WR_REG(0xB7);			
	LCD_WR_DATA8(0x35);

	LCD_WR_REG(0xBB);			
	LCD_WR_DATA8(0x32); //Vcom=1.35V
					
	LCD_WR_REG(0xC2);
	LCD_WR_DATA8(0x01);

	LCD_WR_REG(0xC3);			
	LCD_WR_DATA8(0x15); //GVDD=4.8V  颜色深度
				
	LCD_WR_REG(0xC4);			
	LCD_WR_DATA8(0x20); //VDV, 0x20:0v

	LCD_WR_REG(0xC6);			
	LCD_WR_DATA8(0x0F); //0x0F:60Hz        	

	LCD_WR_REG(0xD0);			
	LCD_WR_DATA8(0xA4);
	LCD_WR_DATA8(0xA1); 

	LCD_WR_REG(0xE0);
	LCD_WR_DATA8(0xD0);   
	LCD_WR_DATA8(0x08);   
	LCD_WR_DATA8(0x0E);   
	LCD_WR_DATA8(0x09);   
	LCD_WR_DATA8(0x09);   
	LCD_WR_DATA8(0x05);   
	LCD_WR_DATA8(0x31);   
	LCD_WR_DATA8(0x33);   
	LCD_WR_DATA8(0x48);   
	LCD_WR_DATA8(0x17);   
	LCD_WR_DATA8(0x14);   
	LCD_WR_DATA8(0x15);   
	LCD_WR_DATA8(0x31);   
	LCD_WR_DATA8(0x34);   

	LCD_WR_REG(0xE1);     
	LCD_WR_DATA8(0xD0);   
	LCD_WR_DATA8(0x08);   
	LCD_WR_DATA8(0x0E);   
	LCD_WR_DATA8(0x09);   
	LCD_WR_DATA8(0x09);   
	LCD_WR_DATA8(0x15);   
	LCD_WR_DATA8(0x31);   
	LCD_WR_DATA8(0x33);   
	LCD_WR_DATA8(0x48);   
	LCD_WR_DATA8(0x17);   
	LCD_WR_DATA8(0x14);   
	LCD_WR_DATA8(0x15);   
	LCD_WR_DATA8(0x31);   
	LCD_WR_DATA8(0x34);
	LCD_WR_REG(0x21); 

	LCD_WR_REG(0x29);

ESP32-C3的屏幕初始化代码

esp32代码,通过设计了一个结构体,来存储初始化命令。然后通过循环,依次发送这些命令。

c

typedef struct {
    uint8_t cmd;
    uint8_t data[16];
    uint8_t databytes; //默认是数据长度,如果最高位为1,表示延时; 如果为 0xFF 表示结束命令.
} lcd_init_cmd_t;


DRAM_ATTR static const lcd_init_cmd_t st_init_cmds[] = {
    /* Sleep Out */
    {0x11, {0}, 0x80},
    /* Memory Data Access Control, MX=MV=1, MY=ML=MH=0, RGB=0 */
    {0x36, {0xA0}, 1},
    /* Interface Pixel Format, 16bits/pixel for RGB/MCU interface */
    {0x3A, {0x05}, 1},
    /* Porch Setting */
    {0xB7, {0x35}, 1},
    /* VCOM Setting, VCOM=1.175V */
    {0xBB, {0x32}, 1},
    /* VDV and VRH Command Enable, enable=1 */
    {0xC2, {0x01, 0xff}, 2},
    /* VRH Set, Vap=4.4+... */
    {0xC3, {0x15}, 1},
    /* VDV Set, VDV=0 */
    {0xC4, {0x20}, 1},
    /* Frame Rate Control, 60Hz, inversion=0 */
    {0xC6, {0x0f}, 1},
    /* Power Control 1, AVDD=6.8V, AVCL=-4.8V, VDDS=2.3V */
    {0xD0, {0xA4, 0xA1}, 2},
    /* Positive Voltage Gamma Control */
    {0xE0, {0xD0, 0x08, 0x0E, 0x09, 0x09, 0x05, 0x31, 0x33, 0x48, 0x17, 0x14, 0x15, 0x31, 0x34}, 14},
    /* Negative Voltage Gamma Control */
    {0xE1, {0xD0, 0x08, 0x0E, 0x09, 0x09, 0x15, 0x31, 0x33, 0x48, 0x17, 0x14, 0x15, 0x31, 0x34}, 14},
     /* Sleep Out */
    {0x21, {0}, 0x80},
    /* Display On */
    {0x29, {0}, 0x80},
    {0, {0}, 0xff} //结束命令
};

void lcd_conf_init(void) {
    int cmd = 0;
    //发送初始化命令组
    while (lcd_init_cmds[cmd].databytes != 0xff) {
        lcd_cmd(spi, lcd_init_cmds[cmd].cmd, false);
        lcd_data(spi, lcd_init_cmds[cmd].data, lcd_init_cmds[cmd].databytes & 0x1F);
        if (lcd_init_cmds[cmd].databytes & 0x80) {
            vTaskDelay(100 / portTICK_PERIOD_MS);
        }
        cmd++;
    }
}

现在开始使用了

类似于这个样子,你只需要编写相应的画图函数就可以了。 我测试使用的屏幕是 280x240 的屏幕。而所用的芯片是 st7789v。但是这个芯片对应的标准屏幕是320x240 的,所以需要调整一下坐标。原理如图所示。 参考资料

                               280px                                          
              |<----------------------------------------->|                   
 (0,0)      |                                           |                   
    +---------------------------------------------------------------+   ^     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |-------> |                                           | <------ |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |240px
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    |         |                                           |         |   |     
    +---------------------------------------------------------------+   v     
                                                                              
    |                             320px                             |         
    |<------------------------------------------------------------->|
c
void lcd_cmd(spi_device_handle_t spi, const uint8_t cmd, bool keep_cs_active)
{
    esp_err_t ret;
    spi_transaction_t t;
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length = 8;                   //Command is 8 bits
    t.tx_buffer = &cmd;             //The data is the cmd itself
    t.user = (void*)0;              //D/C needs to be set to 0
    if (keep_cs_active) {
        t.flags = SPI_TRANS_CS_KEEP_ACTIVE;   //Keep CS active after data transfer
    }
    ret = spi_device_polling_transmit(spi, &t); //Transmit!
    assert(ret == ESP_OK);          //Should have had no issues.
}

void lcd_data(spi_device_handle_t spi, const uint8_t *data, int len)
{
    esp_err_t ret;
    spi_transaction_t t;
    if (len == 0) {
        return;    //no need to send anything
    }
    memset(&t, 0, sizeof(t));       //Zero out the transaction
    t.length = len * 8;             //Len is in bytes, transaction length is in bits.
    t.tx_buffer = data;             //Data
    t.user = (void*)1;              //D/C needs to be set to 1
    ret = spi_device_polling_transmit(spi, &t); //Transmit!
    assert(ret == ESP_OK);          //Should have had no issues.
}

void LCD_Address_Set(spi_device_handle_t spi, uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2)
{

    uint8_t pData[4];
    uint16_t tmp;
    lcd_cmd(spi,0x2a,true);//列地址设置
    tmp = x1+20;      //这里由于屏幕的宽度是 280px,所以需要调整一下坐标
    //这里的 20 是根据屏幕的宽度来调整的。如果你的屏幕宽度是 320px,那么这里的 20 就不需要调整了。
    pData[0]=tmp>>8;  //注意事项1:这里的 pData[0] 和 pData[1] 是高位在前,低位在后。
    pData[1]=tmp;
    tmp = x2+20;
    pData[2]=tmp>>8;
    pData[3]=tmp;
    lcd_data(spi,(uint8_t *)pData,4);


    lcd_cmd(spi,0x2b,true);//行地址设置
    tmp = y1;
    pData[0]=tmp>>8;
    pData[1]=tmp;
    tmp = y2;
    pData[2]=tmp>>8;
    pData[3]=tmp;
    lcd_data(spi,(uint8_t *)pData,4);
    
}

void LCD_Fill(spi_device_handle_t spi, uint16_t xsta,uint16_t ysta,uint16_t xend,uint16_t yend,uint16_t color)
{          
	uint16_t i,j; 
    uint16_t color1=color;
    spi_device_acquire_bus(spi, portMAX_DELAY);
	LCD_Address_Set(spi,xsta,ysta,xend-1,yend-1);//设置显示范围

    lcd_cmd(spi,0x2c,true);//储存器写
	for(i=ysta;i<yend;i++)
	{													   	 	
		for(j=xsta;j<xend;j++)
		{
			// LCD_WR_DATA(color);
			lcd_data(spi,(uint8_t *)&color1,2);
		}
	} 	
    spi_device_release_bus(spi);				  	    
}

void app_main(void)
{
    LCD_Fill(spi,0,0,280,240,0x0000);//全部清屏
    LCD_Fill(spi,50,50,180,140,0xFA00);  //画一个矩形
}

注意事项:

  1. 因为我使用的芯片是ESP32-C3,这是一款基于其 RISC-V 架构,决定了这个芯片执行的是小端模式,所以不能直接使用uint16_t来存储这个数据,因而改成现在这个样子。
  2. 以上示例代码,是按最原始的写法来写的,完全没有考虑速度的问题。都是按同步写法写的,也就是使用spi_device_polling_transmit这个同步函数写的。所以在使用之前,都需要使用spi_device_acquire_busspi_device_release_bus来获取和释放总线。查看参考