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); //画一个矩形
}
注意事项:
- 因为我使用的芯片是ESP32-C3,这是一款基于其 RISC-V 架构,决定了这个芯片执行的是
小端模式
,所以不能直接使用uint16_t来存储这个数据,因而改成现在这个样子。 - 以上示例代码,是按最原始的写法来写的,完全没有考虑速度的问题。都是按同步写法写的,也就是使用
spi_device_polling_transmit
这个同步函数写的。所以在使用之前,都需要使用spi_device_acquire_bus
和spi_device_release_bus
来获取和释放总线。查看参考