有天逛gitee时发现有个叫“黑白菜”的UP的仓库里有F1C200S的一个库,看了下介绍,说是参照一些大佬的作品做出来的,感兴趣就试了试,的确挺好用的,地址在这里F1C200S,不知道这位兄弟在不在这个论坛,献上我对他和其他大佬们的敬意。
用了2天“黑白菜”的库后,我发现里面有初始化8080屏幕接口的函数,还有写入TCON0的0x64寄存器的函数。熟悉的小伙伴看见这个地址就知道这是专门发送8080命令给LCD用的,但是怎么用却没有提供例程,没办法自己摸索呗,以下是我这个初学者这几天摸索的结果,说的不对的地方请各位大佬指正。
一、TCON是自动按照时序发送命令给LCD的。
我第一次调通的时候也是很惊讶,当然这和我没有用过太多的SOC有关,初学者一枚。也就是说F1C200S和8080屏幕相连时,几个重要的管脚顺序如下(以RGB屏幕类比):
CS = VSYNC。
RD = HSYNC。
RS = DE。
WR = CLK。
这也就是用户手册中TCON0的0x68寄存器中提到的IO3~IO0几个管脚。其实对照T113或者D1H的手册中关于屏幕接线的图就不难发现的,估价大佬们都知道的,我是无意间发现,再通过实验证实的,这里就不长篇大论了,直接说结论:
举个例子我想发送一条指令给LCD,比如是设置屏幕横向尺寸的0x2A指令,一般情况下的指令序列用伪指令应该是。
CS = 0
WR = 0
RS = 0 // 发送指令为低,数据为高
D0~D15 = 0x2A
WR = 1
CS = 1
但是现在只需要给TCON0的0x64寄存器发送这个0x2A指令,接着设置0x60寄存器的第25位为低或者高,告诉TCON我们发送的是“命令”还是“数据”,那么TCON会按照上面伪指令的时序自动调整CS、WR管脚(读取的时候是RD管脚),比我们自己写指令方便多了,这在F1C200S的用户手册里没写,我是参照T113的用户手册看的,用“黑白菜”库里的代码如下:
void de_lcd_8080_write(uint16_t data, bool is_cmd) {
while(read32(TCON_BASE + TCON0_CPU_INTF) & 0x00C00000)
;
if(is_cmd) {
clear32(TCON_BASE + TCON0_CPU_INTF, (1 << 25));
} else {
set32(TCON_BASE + TCON0_CPU_INTF, (1 << 25));
}
while(read32(TCON_BASE + TCON0_CPU_INTF) & 0x00C00000)
;
uint32_t reg_data = ((data & 0xfc00) << 8) | ((data & 0x0300) << 6) | ((data & 0x00e0) << 5) | ((data & 0x001f) << 3);
write32(TCON_BASE + TCON0_CPU_WR_DAT, reg_data);
}
我手头只有一台简单的示波器,没有逻辑分析仪,慢慢的一点点试出来的。
基于此,初始化屏幕就变得异常简单了,代码如下:
void lcdSetWindows(uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{
uint16_t temp;
// 横向
temp = x + width - 1;
if (temp >= LCD_WIDTH)
{
temp = LCD_WIDTH - 1;
}
de_lcd_8080_write(0x2a, true);
de_lcd_8080_write(x >> 8, false);
de_lcd_8080_write(x & 0xFF, false);
de_lcd_8080_write(temp >> 8, false);
de_lcd_8080_write(temp & 0xFF, false);
// 纵向
temp = y + height - 1;
if (temp >= LCD_HIGHT)
{
temp = LCD_HIGHT - 1;
}
de_lcd_8080_write(0x2b, true);
de_lcd_8080_write(y >> 8, false);
de_lcd_8080_write(y & 0xFF, false);
de_lcd_8080_write(temp >> 8, false);
de_lcd_8080_write(temp & 0xFF, false);
// 开始写入屏幕
de_lcd_8080_write(0x2c, true);
}
// 寄存器0x36掩码
#define MADCTL_MY 0x80 // 纵向交换
#define MADCTL_MX 0x40 // 横向交换
#define MADCTL_MV 0x20 // 纵横交换
#define MADCTL_ML 0x10 // 从下到上刷新
#define MADCTL_RGB 0x00 // RGB
#define MADCTL_BGR 0x08 // BGR
#define MADCTL_MH 0x04 // 从右到左刷新
void lcdSetDirection(eLcdDirection dir)
{
uint8_t temp = 0;
switch (dir)
{
case LCD_DIRECTION_0:
temp = MADCTL_MX;
break;
case LCD_DIRECTION_90:
temp = MADCTL_MV;
break;
case LCD_DIRECTION_180:
temp = MADCTL_MY;
break;
case LCD_DIRECTION_270:
temp = MADCTL_MY | MADCTL_MX | MADCTL_MV;
break;
default:
break;
}
temp |= MADCTL_BGR;
de_lcd_8080_write(0x36, true);
de_lcd_8080_write(temp, false);
lcdSetWindows(0, 0, LCD_WIDTH, LCD_HIGHT);
}
void lcdFill(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color)
{
lcdSetWindows(x, y, width, height);
for (int y = 0; y < width; y++)
{
for (int x = 0; x < height; x++)
{
de_lcd_8080_write(color, false);
}
}
}
void lcdInit()
{
// 屏幕RESET端口为PE7
gpio_pin_init(GPIOE, 7, GPIO_MODE_OUTPUT, GPIO_PULL_NONE, GPIO_DRV_3);
gpio_pin_set(GPIOE, 7);
delay_soft(2); // 2 * 7 = 14ms
gpio_pin_clear(GPIOE, 7);
delay_soft(3); // 3 * 7 = 21ms
gpio_pin_set(GPIOE, 7);
delay_soft(20); // 20 * 7 = 140ms
// 发送初始化命令
uint8_t initCmd[] = {
0x81, 0x11,
2, 0xfb, 0xc3,
2, 0xfb, 0x96,
2, 0x36, 0x48,
2, 0x3a, 0x55,
2, 0xb4, 0x01,
2, 0xb7, 0xc6,
9, 0xe8, 0x40, 0x8a, 0x00, 0x00, 0x29, 0x19, 0xa5, 0x33,
2, 0xc1, 0x06,
2, 0xc2, 0xa7,
2, 0xc5, 0x18,
15, 0xe0, 0x0F, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2f, 0X54, 0x42, 0x3c, 0x17, 0x14, 0x18, 0x1b,
15, 0xe1, 0xf0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2d, 0x43, 0x42, 0x3b, 0x16, 0x14, 0x17, 0x1b,
2, 0xeb, 0x3c,
0x82, 0xeb, 0x69,
1, 0x29,
0x00};
const uint8_t *cmdIndex = initCmd;
uint8_t count, temp;
while (*cmdIndex)
{
temp = *cmdIndex++;
count = (temp & 0x7F) - 1;
de_lcd_8080_write(*cmdIndex++, true);
for (size_t index = 0; index < count; ++index)
{
de_lcd_8080_write(*cmdIndex++, false);
}
if (temp >= 0x80)
{
delay_soft(20); // 20 * 7 = 140ms
}
}
// 设置屏幕方向为横屏
lcdSetDirection(LCD_DIRECTION_90);
// 清除屏幕
lcdFill(0, 0, LCD_WIDTH, LCD_HIGHT, COLOR565_BLACK);
}
二、TCON、DEFE、DEBE的关系。
这个很好理解,手册里也说了,DEFE和DEBE可以对送过来的图像进行处理,然后送给TCON发送到LCD,今天先说TCON和DEBE。
三、屏幕的颜色模式怎么选择。
我的屏幕是一块普通的8080接口的4寸屏幕,最大16位传输(18位被厂家屏蔽了),RGB565格式,当然通过屏幕的最后三个管脚M0~M2可以让它成为8位或者9位传输模式(RGB666或者RGB888模式)。为了最大传输效率,我选择了16位RGB565模式。
开发板是自己设计并焊接的,用起来效果还行吧,原本开发板搭载的是RGB屏幕,但是为了8080屏幕,自己又设计了块转接板。
那么如何让TCON知道我的屏幕模式呢?请看TCON0的0x60寄存器的第31~29位,这些位就是用来设置屏幕格式的,其中的18、9、8位模式都好理解,为啥16位要有4种颜色模式呢,我的屏幕到底是哪种呢?
这个问题也是无意间解决的,就在第二天我无意间翻阅了T113的“LCD开发指南”的43页,猜我发现了什么?
18bit/1cycle (RGB666)
16bit/3cycle (RGB666)
16bit/2cycle (RGB666)
16bit/2cycle (RGB666)
16bit/1cycle (RGB565)
9bit/1cycle (RGB666)
8bit/3cycle (RGB666)
8bit/2cycle (RGB565)
是不是和TCON0的0x60寄存器的第31~29位对应上了,其中cycle的意思是写入数据需要几个时钟周期,比如我的屏幕是一次可以写入16位数据,所以是“16bit/1cycle (RGB565)”这个模式,按照F1C200S的用户手册相关内容写入“100”给第31~29位,这样颜色模式的问题就解决了。幸好显示接口变化不大,要不然还真不好找呢。
四、TCON和DEBE。
不知道大伙还记不记得,DEBE有个设置图层(Layer)的功能,一共有4个图层,我们一般用Layer0即第一层,通过DEBE的相关功能可以设置这个图层的大小、位置等,最后再用它的0x850寄存器绑定一个我们自己定义的数组给它(暂时叫它Buffer0)。这样我们修改自己的数组Buffer0的内容,等于间接修改了Layer0的内容。但是到这里我就不太明白了,修改了图层后,图层怎么传给TCON呢?RGB屏幕的话我们就不用管理了,TCON会自动处理。到了8080接口这里是不是也是这样呢?通过实验,我发现TCON0的0x60寄存器的第28、27位比较可疑,当然手册里面也写了,但是写的很模糊。第28位是启动自动刷新,第27位则是启动直接刷新,抱着试试看的态度,我将Buffer0的所有数据填充成某种颜色(比如红色),然后通过TCON发送指令0x2A、0x2B和0x2C指令给LCD(即设置写入窗口大小并打开LCD的数据写入功能),再设置TCON0的0x60寄存器的第28或27位设为1时,TCON自动的将Buffer0里面的数据传送给了LCD。我继续让程序每秒动态修改Buffer0的里面的颜色值,随后在LCD上显示出来的也就是写入的颜色了,而且速度很快(不知道是不是内部用了DMA),超过了LCD的液晶的反应速度(不知道这样说对不对),以至于我不得不加入了每帧7ms的延时,就这样也超过了每秒百帧以上的速度(不用延时帧率更高)。不过出来的图像因为液晶速度的原因会产生“Tear Effect”现象,幸好我的这个屏幕提供了一个FMARK管脚,用于同步处理TE信号,这都是后面要研究的了,代码片段如下:
void lcdDirectFlush(bool enabled)
{
if (enabled)
{
set32(TCON_BASE + TCON0_CPU_INTF, (1 << 27));
}
else
{
clear32(TCON_BASE + TCON0_CPU_INTF, (1 << 27));
}
}
...
lcdSetWindows(0, 0, LCD_WIDTH, LCD_HIGHT);
// de_lcd_8080_auto_mode(true);
lcdDirectFlush(true);
uint16_t rectX, rectY, rectWidth, rectHeight;
for(index = 0; index < testCount; ++index)
{
rectX = rand() % (LCD_WIDTH >> 1);
rectY = rand() % (LCD_HIGHT >> 1);
rectWidth = rand() % LCD_WIDTH;
if(rectX + rectWidth >= LCD_WIDTH)
{
rectWidth = LCD_WIDTH - rectX - 1;
}
rectHeight = rand() % LCD_HIGHT;
if(rectY + rectHeight >= LCD_HIGHT)
{
rectHeight = LCD_HIGHT - rectY - 1;
}
disp_fill(rectX, rectY, rectWidth, rectHeight, colorList[index % colorCount]);
delay_soft(1); // 1 * 7 = 7ms
}
这就是我这几天的一点感受,虽然很累但是觉得很值得。再次感谢“黑白菜”和论坛里各位大佬的不懈努力。不知道我以上的表述的清楚不清楚,请各位大佬批评指正。
PS:我已经整理好例程,向“黑白菜”提出了Pull requests。如果着急的话可以直接下载如下的代码压缩包,代码使用方法,先下载“黑白菜”的库,烧写它的BootLoader,然后将我的代码解压到库的“examples”文件夹里,进入文件夹后make download即可,我用的是串口0,需要改变可以修改system.c文件,记得让开发板提前进入FEL模式。
display8080.tar.gz
最近编辑记录 william9527 (2025-05-24 18:13:19)
离线