ST7789 IPS屏幕驱动移植总结
ST7789 IPS屏幕驱动移植总结
本文详细介绍了ST7789 IPS屏幕的驱动移植过程,包括硬件原理图分析、软件配置要点、性能优化技巧以及一些实用的小工具和镜像翻转解决方案。内容主要面向嵌入式开发工程师,对于想要深入了解屏幕驱动实现细节的技术人员具有较高的参考价值。
原理图分析
屏幕总共引出12个pin,最终需要软件控制的也就5个pin(BLK, DC, RES, SDA, SCL),需要注意的是,屏幕后面有个背光灯,只有打开之后屏幕才能正常显示,LEDK是灯的阴极,LEDA是灯的阳极,由图可知阳极已经接到VCC,因此通过控制LEDK接地,背光灯打开,接高,背光灯关闭。
- LEDA和VDD均接到外部给的VCC(3.3V),为什么要添加C1电容?电容隔直流通交流,也就是VCC中的高频成分会通过电容流入地,稳定的直流成分会稳稳的给到屏幕
- BLK是怎么控制背光灯关闭/打开的,当BLK为高电平或者悬空时,没有电流流过R2,R3,即三极管的基极为高,三极管导通,LEDK拉低,所以背光灯会亮,相反,如果BLK为低,那么基极电压在3.3 * (1/11) = 0.3V < 0.7V,那么三极管截止,相当于LEDK断开,背光灯就不会打开,因此得出结论 BLK为高或悬空时,背光灯打开,BLK为低时,背光灯关闭
踩坑记录
- STM32H750/743配置SPI时需要设置
SPI_MASTER_KEEP_IO_STATE_ENABLE
,否则SPI的SCK在发送完一个字节后会有抖动,因为发送完成之后,SCK等pin会被释放成高阻(CubeMX居然不是把SPI_MASTER_KEEP_IO_STATE_ENABLE作为默认值…) - 不同的LCD需要区分使用的是
SPI MODE0
还是SPI MODE3
,虽然都是上升沿采样,但是SCK的空闲电平不一样,如果选错模式的话,那么即使命令参数都设置对,LCD也无法显示,最好还是看商家给的demo是用的啥模式,如果demo是软件模拟的SPI,那只能分析波形了
优化
如何最大限度的去提升每秒屏幕能刷的帧数,下面是我想到的两种办法:
提升SPI速率
这是我用的屏幕的电气特性,可以看到在往LCD写入数据时,SCL的周期Tscycw最小值是16ns,反推出在写入数据时SCL的最大频率在62.5Mhz,因此想追求更高的屏幕刷新率,可以将SPI SCL的时钟往最大值上靠,例如,我用的240 * 240的屏幕,刷新一帧屏幕的RGB565的像素点需要靠SPI发送240 * 240 * 2 = 115200
字节的数据,如果SPI的速度频率在60Mhz,那么最快屏幕每秒能刷新(60*1000*1000) / (240*240*2*8) = 65
帧。改用SPI DMA的方式传输
最显而易见的是,使用DMA传输之后,能够释放CPU的资源去干其他事,而不用干耗着去传输大量的像素点数据,所以很多商家给的demo程序也是用DMA实现的。但是
,虽然用了DMA,在处理像素点数据的时候会将高字节的数据和低字节的数据进行换位,比如rgb565格式的一个像素点,总共是2个字节,为了满足协议要求(先发送高字节再发送低字节)往往会对每个像素点进行换位,虽然这样做没有问题,但是无形中加重了cpu的负担,如何解决这个问题呢?出现这样的问题,是因为将SPI DATASIZE设置为8bits,然后DMA按照8bits的宽度进行搬运,会按照内存地址的顺序(小端)去挨个发送数据,但是当遇到一个u16的像素点,继续这样发送就与手册规定的顺序要求相反了,解决办法如下: 当发送LCD命令和参数时,配置SPI的DATASIZE为8bits,然后使用spi的轮询方式发送(数据量小,占用的时间也少),当发送像素点数据时,切换SPI的DATASIZE为16bits,然后使用SPI的DMA方式进行发送(这样就会先发像素点的高字节) ,下面以填充LCD RAM的用到的函数演示上方的方案:
#define LCD_SPI_SWITCH_TO_DATASIZE_8BIT() \
do { \
hspi1.Init.DataSize = SPI_DATASIZE_8BIT; \
hspi1.Instance->CFG1 = (hspi1.Instance->CFG1 & ~0x1F) | hspi1.Init.DataSize; \
} while (0)
#define LCD_SPI_SWITCH_TO_DATASIZE_16BIT() \
do { \
hspi1.Init.DataSize = SPI_DATASIZE_16BIT; \
hspi1.Instance->CFG1 = (hspi1.Instance->CFG1 & ~0x1F) | hspi1.Init.DataSize; \
} while (0)
// 发送控制LCD的命令和参数
static void lcd_control(uint8_t cmd, uint8_t *data, uint16_t data_len)
{
LCD_SEND_CMD_BEGIN();
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
LCD_SEND_DATA_BEGIN();
if ((data != NULL) && (data_len > 0)) {
HAL_SPI_Transmit(&hspi1, data, data_len, HAL_MAX_DELAY);
}
}
// 设置LCD RAM的填充区域
static void lcd_address_set(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end)
{
// set spi datasize to 8bit
LCD_SPI_SWITCH_TO_DATASIZE_8BIT();
// column address set
lcd_control(0x2A, (uint8_t[]){(x_start >> 8) & 0xFF, x_start & 0xFF, (x_end >> 8) & 0xFF, x_end & 0xFF}, 4U);
// row address set
lcd_control(0x2B, (uint8_t[]){(y_start >> 8) & 0xFF, y_start & 0xFF, (y_end >> 8) & 0xFF, y_end & 0xFF}, 4U);
// memory write
lcd_control(0x2C, NULL, 0U);
}
*******************************************************************************
* @brief Lcd display flush
*
* @param[in] x_start x start position
* @param[in] y_start y start position
* @param[in] x_end x end position
* @param[in] y_end y end position
* @param[in] color_data Pointer to color data
*******************************************************************************
*/
void lcd_display_flush(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t *color_data)
{
while (!spi_dma_transfer_done); // 等待上一次传输完成
lcd_address_set(x_start, y_start, x_end, y_end);
// set spi datasize to 16bit
LCD_SPI_SWITCH_TO_DATASIZE_16BIT();
spi_dma_transfer_done = 0U;
HAL_SPI_Transmit_DMA(&hspi1, (const uint8_t *)color_data, (x_end - x_start + 1) * (y_end - y_start + 1));
}
小工具
为了更方便的提取图片的像素点出来,我用python实现了个小工具,打包成picutil.exe放在这儿,用法如下
输入想要截取的范围以及对应的png图片,最终会生成一个C语言的数组,包含该区域的rgb565格式的像素点,类似于下面这样
把这个数组拷贝进工程里编译就行了,但是得注意,这数组会编译后直接存放进芯片的内部flash,所以得注意大小, 别超出内部flash空间最大范围
镜像翻转
当使用分光棱镜放在屏幕上时,会发现分光棱镜显示的图片与屏幕上的图片呈现出一种上下翻转的效果,如何能让分光棱镜显示出咱正常想要的效果呢?如果自个写套算法实现上下翻转未免有点耗费CPU了,好在发现这块屏幕有图片翻转的功能。
命令MADCTL(0x36)的参数中有控制LCD ram中像素点存放方式的bit,即D7, D6, D5。这三个bit组合起来使用的效果如下:
显然,使用Y-Mirror就能实现咱想要的效果(cmd:0x36 parameter:0x80
),但还有个点需要注意
lcd内部ram是240 * 320的,然而我使用的屏幕是240 * 240的,使用了Y-mirror之后,发送的像素点数据会从lcd ram的左下角位置开始填充,此处的数据在屏幕上无法显示,因此需要将Y轴的像素点位置整体向上偏移320 - 240 = 80
个像素点,对于代码修改如下:
void lcd_display_flush(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t *color_data)
{
while (!spi_dma_transfer_done); // 等待上一次传输完成
// Y轴整体向上移80个像素点
lcd_address_set(x_start, y_start + 80, x_end, y_end + 80);
// set spi datasize to 16bit
LCD_SPI_SWITCH_TO_DATASIZE_16BIT();
spi_dma_transfer_done = 0U;
HAL_SPI_Transmit_DMA(&hspi1, (const uint8_t *)color_data, (x_end - x_start + 1) * (y_end - y_start + 1));
}