首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

FSMC驱动8080接口液晶屏

显示器是计算机的一个主要输出设备。对于一个嵌入式的系统而言,往往不需要显示器。但是一旦涉及到比较复杂的人机交互过程, 就需要有一个显示器输出一些提示信息以及系统状态。目前触摸屏已经很普及了,它不仅是一个输出设备了, 而且是一个输入设备,多点触摸的手机已经不是什么新鲜玩意儿了,很多显示器也已经具备触摸功能的了。这使得人机交互过程更加便捷。

显示器也是由多个模块构成的,为了满足用户的需求,厂商设计和制造了各种接口形式的显示器,目前市面上见到的基本都是LCD显示器了。 他把LCD控制器、驱动器和显示屏集成在一起,用户只需要把LCD控制器的接口与处理器的接口简单连接起来,并通过LCD的指令系统编写程序, 就可以完成复杂界面的显示工作了。

LCD的全称是Liquid Crystal Display,它通过液晶和彩色滤光器过滤光源,进而在平板上显示图像。 在探索者的开发板上,有一个8080接口的液晶屏。 本文就结合原开发板的例程,了解一下液晶屏的驱动过程。

1. 液晶显示的基本原理

根据维基的说法, 1988年奥地利的植物学家和化学家Friedrich Reinitzer从胡萝卜中提取出了一种化合物。这种化合物具有两个不同温度的熔点, 在一定的温度下具有固体和液体的双重特性,之后人们就将之称为"液晶"。

之后的几十年的时间里,也有一些科学家研究液晶的各种特性。直到1960年代,美国RCA公司的工程师们发现不同的电压可以改变液晶分子的排列状态, 并让射入的光线发生偏转。利用这一特性,他们发明了世界上第一台液晶屏。

将液晶材料置于两块光轴垂直的偏光板之间,如果两块偏光板之间没有形成电场,光线从一侧偏光板射入后,会沿着液晶旋转的方向前进到达另一侧的偏光板射出。 当两块偏光板之间有电场,就会改变液晶分子的排列状态,使得光线无法从另一侧的偏光板射出。这一现象称作扭转式向列场效应(Twisted Nematic Feild Effect, TNFE)。

使用液晶屏就需要一个背板光源,再加上彩色滤光板。我们可以用一个阵列的形式,通过控制阵列中各个单元的电压,就可以控制各个像素的明亮和色彩。 最后就可以形成想要的图像了。根据驱动形式的不同,有扭转式向列型(Twisted Nematic, TN)、超扭转式向列型(Super Twisted Nematic, STN)、 薄膜式晶体管型(Thin Film Transistor, TFT)。

2. ATK-4.3' TFTLCD电容触摸屏

这里我们选用为探索者配套的ATK-4.3' TFTLCD电容触摸屏(以后简称ATK4.3)为研究对象入门液晶屏的驱动控制。它是一块由NT35510驱动的,16位真彩800×480的液晶屏。 由于NT35510自带GTRAM,所以不需要额外的器件,MCU就可以直接驱动。该模块还有支持5点触摸,我们将在后续章节中予以介绍。

NT35510是一款带显存的集驱动器与控制器于一身的TFT LCD的单芯片解决方案。它支持多种分辨率,因为它内置了480×864×24位的显存, 所以可以为480RGB×864、480RGB×854、480RGB×800、480RGB×720和480RGB×640提供内置的CGRAM。而对于为480RGB×1024,NT35510提供了扩展接口可以通过旁路CGRAM予以支持。

NT35510支持MDDI接口、MIPI接口、16/18/24位RGB接口、8/16/24位80系统接口、SPI和I2C接口。使得我们可以根据需要选择合适的接口控制LCD。 ATK4.3采用16位的8080并口与外部连接,其在探索者开发板上的接口原理图如下图所示。

  • CS: LCD片选信号
  • WR: 向LCD写入数据
  • RST: 硬件复位LCD
  • D[15:0]: 16位双向数据线
  • BL: 背光控制。在ATK4.3中该引脚已经自带了一个100k的下拉电阻,所以如果该引脚悬空是不会有背光的。
  • RS: 命令/数据标识(0,读写命令;1,读写数据)
  • RD: 从LCD读取数据
  • MISO:
  • MOSI:
  • T_PEN: 电容触摸屏中断信号
  • T_CS: 电容触摸屏复位信号
图 1. 探索者LCD原理图
8080并口是由Intel开发并广泛用于各类液晶显示器的接口。它有5个控制管脚:CS用作片选信号,RS为命令/数据标识位, WR指示写入数据,RD指示读取数据, RST用于复位LCD。其读/写过程如下:
  1. 根据将要读写的数据类型,设置RS电平
  2. 拉低片选信号CS,选中LCD驱动器
  3. 如果需要读取数据则将RD拉低,如果需要写入数据将WR拉低。
  4. 在RD的上升沿读取数据线D[15:0]上锁存的数据,在WR的上升沿写数据到D[15:0]上。
相关控制信号可以用下表表示:

表 1 8080控制引脚电平信号

RS CS WR RD
写命令 L L H
读状态 L L H
写数据 H L H
读数据 H L H

在8080的控制接口定义中,读操作和写操作分别用WRRD控制着, 在探索者的原理图中它们被分别接到了FSMC_NWE和FSMC_NOE上,整个控制时序与RAM的时序类似。所以在探索者开发板的设计中,是希望将之看作是一块内存, 通过FSMC来访问NT35510。

再看原理图,我们可以看到LCD模组的16位数据线都接到了FSMC的数据线上了,而且是一一对应的。LCD的片选信号接到了FSMC_NE4上,这意味着LCD被映射到了BANK1的区域4, 对应的首地址为0x6C000000。但是只接有一个地址线A6与RS连接,而数据总线又是16位,所以地址的第8位决定了对LCD的一次访问是指令操作还是数据通信, 即0x6C000080是数据通信,0x6C00007E则是指令操作。

此外,根据液晶屏的原理介绍我们知道,要让液晶屏能够正常的工作还需要一个背光,所以在液晶屏模组上还有一个BL的引脚接到了F407的一个IO端口上。在模组上, 已经为BL引脚添加了一个100k的下拉电阻,所以默认情况下背光是关闭的,需要在MCU上通过一个引脚控制打开背光。

3. 模组的背光控制

模组的背光实际上就是一个LED,完全可以参照通用IO端口驱动LED灯操作LCD的背光。 下面通过函数lcd_init_bl()完成初始化操作,查看原理图可以看到LCD_BL接到了PB15引脚上。在函数中,我们首先开启GPIOB的驱动时钟,再配置PB15引脚工作在输出模式下。 因为在模组上已经有了一个下拉电阻,所以这里我们以推挽输出的方式驱动背光,不再开启IO端口的上下拉电阻了。

        void lcd_init_bl(void) {
            RCC->AHB1ENR.bits.gpiob = 1;
            
            GPIOB->MODER.bits.pin15 = GPIO_Mode_Out;
            GPIOB->OTYPER.bits.pin15 = GPIO_OType_PP;    
            GPIOB->PUPDR.bits.pin15 = GPIO_Pull_No;
            GPIOB->OSPEEDR.bits.pin15 = GPIO_OSpeed_Very_High;
        }
然后我们为LCD_BL定义一个宏
        #define LCD_BL (PBout(15))
以后,就可以通过这个宏来开关LCD的背光了:
        LCD_BL = 1; // 打开背光
        LCD_BL = 0; // 关闭背光

4. FSMC访问NT35510

根据对原理图的分析,我们需要配置20个引脚来通过FSMC访问NT35510,其中16个数据总线,1个地址总线,以及3个控制信号。一共涉及到D、E、F、G四个GPIO端口。 我们先在函数lcd_init_gpio()中完成对引脚的初始化工作,首先开启四个GPIO端口和FSMC的驱动时钟:

        void lcd_init_gpio(void) {
            RCC->AHB1ENR.bits.gpiod = 1;
            RCC->AHB1ENR.bits.gpioe = 1;
            RCC->AHB1ENR.bits.gpiof = 1;
            RCC->AHB1ENR.bits.gpiog = 1;
            RCC->AHB3ENR.bits.fsmc = 1;
接着配置相关引脚的工作方式为FSMC,下面截取了部分引脚的功能映射的代码:
            GPIOD->AFR.bits.pin0 = 12;  // PD0 -> FSMC_D2
            GPIOD->AFR.bits.pin1 = 12;  // PD1 -> FSMC_D3
            GPIOD->AFR.bits.pin4 = 12;  // PD4 -> FSMC_NOE
            GPIOD->AFR.bits.pin5 = 12;  // PD5 -> FSMC_NWE
            // 其它相关引脚的复用功能配置省略了,详细参见源码
最后,配置各个引脚工作在推挽复用的模式下,并开启上拉电阻:
            struct gpio_pin_conf pincof;
            pincof.mode = GPIO_Mode_Af;
            pincof.otype = GPIO_OType_PP;    
            pincof.pull = GPIO_Pull_Up;
            pincof.speed = GPIO_OSpeed_Very_High;
            gpio_init(GPIOD, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_8
                    | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_14 | GPIO_Pin_15, &pincof);
            gpio_init(GPIOE, GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11
                    | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15, &pincof);
            gpio_init(GPIOF, GPIO_Pin_12, &pincof);
            gpio_init(GPIOG, GPIO_Pin_12, &pincof);
        }

完成了对各个引脚的初始化之后,我们就可以配置FSMC的寄存器了。因为LCD挂在Bank1的第4个区域,所以在函数lcd_init_fsmc()中我们只对BCR4,BTR4和BWTR4进行配置。 首先我们需要开启FSMC的驱动时钟并对寄存器清零。

        void lcd_init_fsmc(void) {
            RCC->AHB3ENR.bits.fsmc = 1;
            // 寄存器清零
            FSMC_Bank1->bcr4.all = 0;
            FSMC_Bank1->btr4.all = 0;
            FSMC_Bank1E->bwtr4.all = 0;
根据探索者开发教程的说法,TFTLCD的读操作一般比较慢,而写操作比较快。 如果两者使用相同的时序设定,要么让写操作与读操作一致也以较慢的时序工作,要么频繁地为读写操作配置FSMC过程比较繁琐。好在FSMC地模式A支持独立地读写操作,所以这里需要使用扩展功能。
            FSMC_Bank1->bcr4.bits.WREN = 1;                     // 写使能
            FSMC_Bank1->bcr4.bits.EXTMOD = 1;                   // 使用扩展功能
            FSMC_Bank1->bcr4.bits.MWID = FSMC_BCR_MWID_16b;     // 总线宽度
接下来配置读操作的时序,拉长它的数据保持时间
            // 读时序操作
            FSMC_Bank1->btr4.bits.ACCMOD = FSMC_BTR_ACCMOD_A;   // 模式A
            FSMC_Bank1->btr4.bits.ADDSET = 15;                  // 地址建立时间15个HCLK, 15/168M ≈ 90ns
            FSMC_Bank1->btr4.bits.DATAST = 60;                  // 数据保持时间60个HCLK, 约为360ns
配置写操作的时序,并使能FSMC。
            // 写时序操作
            FSMC_Bank1E->bwtr4.bits.ACCMOD = FSMC_BTR_ACCMOD_A; // 模式A
            FSMC_Bank1E->bwtr4.bits.ADDSET = 9;                 // 地址建立时间9个HCLK, 约为54ns
            FSMC_Bank1E->bwtr4.bits.DATAST = 8;                 // 数据保持时间8个HCLK, 约为48ns
            // 使能Bank1,区域4
            FSMC_Bank1->bcr4.bits.MBKEN = 1;
        }

在分析原理图的时候我们就说过,只用了一个地址线A6,所以本质上我们的LCD只有命令和数据两个寄存器,分别对应地址0x6C00007E和0x6C000080。 参照其它片上外设的设定,我们定义如下的结构体和宏定义:

        struct LcdDev {
            volatile uint16 cmd;
            volatile uint16 data;
        };
        
        #define LCD_BASE ((uint32)(0x6C000000 | 0x0000007E))
        #define LCD      ((struct LcdDev *) LCD_BASE)
如此一来我们就可以方便的向LCD写入并读出指令和数据了,类似如下的语句:
        LCD->cmd = COMMAND;        
        LCD->data = DATA;

        COMMAND = LCD->cmd;
        DATA = LCD->data;

接下来,我们写一个简单的例程,控制显示器的所有像素点亮并熄灭。首先,调用刚才提到的三个初始化函数,完成系统的配置功能。

        int main(void) {
            lcd_init_bl();
            lcd_init_gpio();
            lcd_init_fsmc();
然后延迟一段时间,等待NT35510上电初始化结束,然后打开背光。
            delay(1680000);
            delay(1680000);
            LCD_BL = 1;
根据NT35510第261页的描述,读出芯片的ID。 默认读出来的结果就是,ID1=0x00, ID2=0x80, ID3=0x00。
            LCD->cmd = 0x0400;
            id1 = LCD->data;
            LCD->cmd = 0x0401;
            id2 = LCD->data;
            LCD->cmd = 0x0402;
            id3 = LCD->data;
在NT35510的文档中说,上电之后NT35510需要先完成硬件的初始化,需要延迟5ms以上。 然后通过指令SLPOUT(0x1100)开启液晶驱动的DC/DC转换器和内部的显示晶振。再延迟5ms以上等待系统稳定之后, 通过指令DISPON(0x2900)开启显示功能,之后就可以控制液晶屏按照需要显示了。这里先后通过指令ALLPON(0x2300)和ALLPOFF(0x2200)控制所有像素点亮或者熄灭, 关于其它指令和像素控制方法可以参考官方文档的说法,我们以后也会有专题介绍。
            LCD->cmd = 0x1100; // SLPOUT
            delay(1680000); delay(1680000);
            LCD->cmd = 0x2900;  // DISPON
            delay(1680000); delay(1680000);
        
            LCD->cmd = 0x2200;  // all pixel off
            delay(1680000); delay(1680000);
            LCD->cmd = 0x2300;  // all pixel on
最后,我们关闭液晶背光结束例程。
            LCD_BL = 0;
            while (1) { }
        }

5. 总结

在本文中,我们通过STM32的FSMC来实现一个8080的接口驱动NT35510。现在不知道总结个啥了,以后再修改吧。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1