STM32CubeMX教程22 FSMC - 8080并行接口TFT-LCD驱动

目录

1、准备材料

2、实验目标

3、实验流程

3.0、前提知识

3.0.1、FSMC概述

3.0.2、为什么使用FSMC控制LCD?

3.0.3、LCD接口硬件原理详解

3.0.3、控制FSMC对LCD命令/数据写入(重点)

3.0.4、FSMC时序参数设置

3.1、CubeMX相关配置

3.1.0、工程基本配置

3.1.1、时钟树配置

3.1.2、外设参数配置

3.1.3、外设中断配置

3.2、生成代码

3.2.0、配置Project Manager页面

3.2.1、外设初始化调用流程

3.2.2、外设中断调用流程

3.2.3、添加其他必要代码

4、烧录验证

5、注释详解


读者可访问 GitHub - lc-guo/STM32CubeMX-Series-Tutorial 获取原始工程代码


1、准备材料

开发板(正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(MDK-Arm

CH340G Windows系统驱动程序(CH341SER.EXE

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板FSMC接口驱动8080并行接口TFT-LCD显示,具体为使用FSMC Bank 1-NOR/PSRAM4,片选信号为NE4(PG12),TFT-LCD的RS引脚接FSMC地址线A6(PF12)来驱动TFT-LCD进行不同颜色刷屏显示

3、实验流程

3.0、前提知识

开发板使用TFT-LCD时最好使用外接电源给开发板供电,如果使用单USB供电可能会出现电流不足电压波动TFT-LCD初始化失败的现象,以下TFT-LCD全部简称为LCD

3.0.1、FSMC概述

FSMC全称为灵活的静态存储控制器,其时钟信号为HCLK,FSMC根据支持的存储器类型可以分为NOR/PSRAM、NAND Flash和PC卡三种,本实验只涉及第一种NOR/PSRAM,严格意义上讲也与NOR/PSRAM无关,只是使用其中的LCD Interface类型的内存类型,下图中2号红框中标出了本此实验需要使用的FSMC的一些引脚,主要包括片选引脚、地址线、数据线、输出和写入等,如下图所示为FSMC的具体框图(注释1)

从FSMC的角度,外部存储器被划分为4个固定大小的存储区域,每个存储区域的大小为256 MB,存储区域1可连接4个NOR Flash或PSRAM存储器器件,此存储区域被划分为4个NOR/PSRAM区域,带4个专用片选信号NE1/2/3/4,每个区域大小为64MB,FSMC的具体存储区域映射如下图所示

通过FSMC写入外部存储设备需要知道写入的地址,该地址有HADDR所确定,HADDR是AHB内部地址线,但也会参与对外部存储器的寻址,其HADDR[27:26]两个位用于选择从地址0X6000 0000到地址0x6FFF FFFF处分成4个部分的存储区域,HADDR[25:0]的26个位用于地址线,确定写入选中存储区域中的地址,如下图所示位HADDR的解释(注释2)

由于 HADDR 为字节地址,而存储器按字寻址,据存储器数据宽度不同,实际向存储器发送的地址也将有所不同,当存储器宽度为16位(16根数据线)时,向存储器发出的数据地址将自动向右偏移一位,具体如下表所

3.0.2、为什么使用FSMC控制LCD?

FSMC一般是用来连接SRAM、PSRAM、ROM和NOR Flash等存储设备的,为什么要使用FSMC来控制8080并口的LCD呢?

先来看一看这些存储设备和8080并口LCD的引脚,SRAM的接口如下图所示(注释3)

8080并口LCD的接口引脚如下图所示

可以发现LCD的接口中除去RST和背光BLK引脚外,其他的引脚均可以由FSMC来提供,RS引脚可以选择FSMC的26位地址线中的一根控制,片选信号可以由FSMC的NEx引脚提供,RD由NOE提供,WR由NWE提供,D0-D15可以与FSMC的16跟数据线直接连接

所以在FSMC的NORFIash/PSRAM/SRAM/ROM/LCDx中也有一个专门的LCD Interface类型的内存类型,因此我们其实可以把8080并口的LCD当成一个存储设备通过FSMC来控制

3.0.3、LCD接口硬件原理详解

  1. 8080并口LCD的片选信号CS可以直接使用FSMC的NEx引脚,这里使用FSMC Bank 1-NOR/PSRAM4来控制LCD,因此CS接FSMC_NE4
  2. LCD写入引脚WR接FSMC的写入使能引脚FSMC_NWE
  3. LCD的读操作引脚接FSMC的输出使能引脚FSMC_NOE
  4. LCD寄存器选择引脚RS接26位地址线上任何一根都可以,这里接入了A6
  5. 16位数据线直接对接
  6. LCD的复位引脚与开发板复位引脚相连,使用开发板复位引脚可以同时复位MCU和LCD
  7. LCD的背光引脚由PB15引脚控制,当高电平时背光被点亮

如下图所示为LCD接口硬件原理图

3.0.3、控制FSMC对LCD命令/数据写入(重点)

使用FSMC写LCD时一般分为写数据(选择数据寄存器)或者写命令(选择控制寄存器),而LCD的RS引脚负责决定选择数据还是控制寄存器,如果RS引脚是高电平1,则选择数据寄存器;如果RS引脚引脚为低电平0,则选择控制寄存器

FSMC的地址线一共有26位(FSMC_A[25:0]),对于LCD来说,只需要任选其中一根地址线与RS引脚连接即可实现对LCD写数据/命令的控制,但是选择不同地址线所对应的控制写命令/数据的地址也不相同

以本实验为例,此时LCD使用FSMC Bank 1-NOR/PSRAM4,其起始地址为0x6C00 0000,LCD的RS引脚接FSMC地址线A6(PF12),且此时LCD为16位宽度数据传输,FSMC会将写入的地址自动向右偏移一位,因此LCD的RS引脚选择控制寄存器只需要在起始地址基础上满足A7位为0即可,比如可以是0x6C00 0000,也可以是0x6C00 0040等;LCD的RS引脚选择写数据寄存器只需要在起始地址基础上满足A7位为1即可,比如可以是0x6C00 0080,也可以是0x6C00 00C0等;

因此我们可以直接定义FSMC写LCD命令/数据的地址,第一种方法源代码如下所示

/*LCD写命令地址*/
#define LCD_REG (*((volatile unsigned short *) 0x6C000000))
/*LCD写数据地址*/
#define LCD_RAM (*((volatile unsigned short *) 0x6C000080))

/*将数据data写入LCD数据寄存器*/
LCD_RAM = data;
/*将寄存器值regval写入LCD控制寄存器*/
LCD_REG = regval;

除了上述的方法外,常见的还有一种定义LCD地址结构体的方法,其原理一致,但读者理解的时候可能不太容易理解,接下来由笔者仔细分析一下,第二种方法源代码如下所示

/*定义LCD地址结构体*/
typedef struct
{
    volatile uint16_t LCD_REG;
    volatile uint16_t LCD_RAM;
}LCD_TypeDef;

/*LCD控制/数据寄存器基址*/
#define LCD_BASE ((uint32_t)(0x6C000000 | 0x0000007E))
#define LCD ((LCD_TypeDef *) LCD_BASE)


/*将数据data写入LCD数据寄存器*/
LCD->LCD_RAM=data;
/*将寄存器值regval写入LCD控制寄存器*/
LCD->LCD_REG=regval;

我们已经知道,当LCD的RS引脚接FSMC地址线A6,由于FSMC会将写入的地址自动向右偏移一位,因此写入数据(选择数据寄存器)时地址线A7应该为1,写入命令(选择控制寄存器)时地址线A7应该为0

而在结构体中先定义了uint16_t(2个字节)的LCD_REG变量,紧接着定义了uint16_t的LCD_RAM变量,这两个变量在内存中的地址应该是连续的

并且我们将LCD变量强制转化为了LCD_TypeDef*类型的变量,同时将LCD_BASE的地址赋值给LCD,因此LCD的地址应该为(0x6C000000 | 0x0000007E)),LCD->LCD_REG的地址应该同样为(0x6C000000 | 0x0000007E)),而LCD->LCD_RAM的地址应该为(0x6C000000 | 0x0000007E+2))

此时读者应该可以发现通过结构体中顺序定义的变量在内存中地址连续这一特点,LCD->LCD_REG表示了地址线A7为0,LCD->LCD_RAM表示了地址线A7为1,也即通过LCD->LCD_REG可以写入LCD命令,通过LCD->LCD_RAM可以写入LCD数据

再举个例子,如果LCD的RS引脚接FSMC地址线的A12呢?

此时写入数据(选择数据寄存器)时地址线A13应该为1,写入命令(选择控制寄存器)时地址线A13应该为0

则按照第一种方法,可定义为如下代码

/*LCD写命令地址*/
#define LCD_REG (*((volatile unsigned short *) 0x6C000000))

/*LCD写数据地址*/
#define LCD_RAM (*((volatile unsigned short *) 0x60002000))

按照第二种方法,可定义为如下代码

/*LCD控制/数据寄存器基址*/
#define LCD_BASE ((uint32_t)(0x6C000000 | 0x00001FFE))
#define LCD ((LCD_TypeDef *) LCD_BASE)

一个小技巧,在使用第二种方法确定 LCD_BASE 地址时,假如RS接FSMC的地址线A6,则从右边往左数到A7位(中间全是1,两边两个0),0111 1110(0x7E);假如RS接FSMC的地址线A12,同样从右边往左数到A13位,01 1111 1111 1110(0x1FFE);

3.0.4、FSMC时序参数设置

查看LCD使用的控制芯片NT35510的芯片数据手册(注释4)

在361页的7.6.1并行接口特性(80 系列 MCU)小节中说明了使用8080并行接口控制的LCD所需要满足的时序和信号特征的最大最小值

如下图中表格所示,重点看表格中读写的地址建立时间和数据建立时间,其规定了最小需要的时间

在STM32CubeMX中可以设置读写时序的地址建立时间和数据建立时间,单位为一个HCLK时钟周期,当HCLK时钟频率为STM32F407最高频率168MHz时,一个周期大约为6us

这里按照手册理论上读地址建立时间>10ns(2个HCLK时钟周期),写地址建立时间>0us,数据建立/保持时间>15+10ns(5个HCLK时钟周期),读写地址保持时间>2ns,但生成代码中读写地址保持时间默认15个HCLK时钟周期,在软件上不可调,另外总线翻转时间设置为0,访问模式选择A即可

因此综上所述,将参数从上到下设置为2、5、0、A、0、5、0和A(笔者在正点原子探索者开发板上使用4.3寸NT35510控制的LCD上验证可以使用),实际使用中如果出现问题可以增大参数值,具体配置如下图所示

再举个例子,如果你的LCD使用的控制芯片为ILI9341,那么找到ILI9341芯片数据手册(注释5)

在手册238页18.3.1小节中找到8080并口控制时的时序参数要求,如下图所示,根据手册我们可以知道理论上读/写地址建立时间>0ns,数据建立/保持时间>10+10ns(4个HCLK时钟周期),读写地址保持时间>0ns

因此在STM32CubeMX中参数从上到下设置为0、4、0、A、0、4、0和A即可(笔者在正点原子探索者开发板上使用野火3.2寸ILI9341控制的LCD上验证可以使用)

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号)选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

单击Pinout & Configuration页面左边Connectivity/FSMC选项,在右边的Mode下点开NORFIash/PSRAM/SRAM/ROM/LCD4选项卡(因为LCD使用的是使用FSMC Bank 1-NOR/PSRAM4),选择片选信号NE4,内存类型为LCD Interface,LCD寄存器选择(RS引脚)选择FSMC地址线A6,数据Data为16位(D0-D15),具体配置如下图所示

然后在Configuration里面配置其读写时序,为什么要这样配置?可以阅读本实验“3.0.4、时序参数设置”小节,具体配置如下图所示

配置好之后可以与硬件原理图上的引脚对照以下,看看有没有漏掉哪些引脚或者那些引脚默认配置不正确,由于RESET引脚直接与MCU的NRST引脚连接,当按下开发板复位按键时,LCD也会被复位,因此最后只需设置LCD的背光控制引脚即可,具体配置如下图所示

3.1.3、外设中断配置

本实验无需配置任何中断

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节

3.2.1、外设初始化调用流程

在STM32CubeMX中配置了FSMC之后,会在生成的工程代码中新增MX_FSMC_Init()函数,在该函数中对FSMC的模式和对FSMC的读写时序参数进行了配置,然后调用了HAL_SRAM_Init()初始化函数

在HAL_SRAM_Init()初始化函数中调用了HAL_SRAM_MspInit(),然后在HAL_SRAM_MspInit()函数中最终调用了HAL_FSMC_MspInit()

在HAL_FSMC_MspInit()函数中完成了FSMC的数据线、地址线、读和写等引脚的复用操作,并且使能了相应的时钟

如下图所示为FSMC初始化具体调用流程

3.2.2、外设中断调用流程

本实验无需配置任何中断

3.2.3、添加其他必要代码

首先需要添加LCD的驱动文件,该驱动文件中应该对LCD进行正常初始化操作,另外应该包含一些LCD的画图函数,比如绘制圆形、直线和矩形等函数,一般是按照LCD驱动芯片的手册进行编程,但是为了避免重复造轮子,这里的驱动文件直接使用正点原子提供的(注释6),笔者对文件稍微整理了以下,具体源代码如下所示

tftlcd.c文件

tftlcd.h文件

font.h文件

向工程中添加.c/.h文件的步骤请阅读“STM32CubeMX教程17 I2C - MPU6050驱动”实验3.2.3小节

值得提醒的是在LCD初始化函数中使用串口打印了LCD的控制芯片ID,因此如果未初始化串口请注释掉该输出代码,如果你不希望注释,请阅读“STM32CubeMX教程9 USART/UART 异步通信”配置并重定向串口

然后在主函数中添加一些用于测试LCD是否正常工作的函数,具体源代码如下所示

/*LCD测试函数*/
void LCD_TEST(void)
{
    /*画线测试*/
    testlines();
    HAL_Delay(1000);
    /*画正方形测试*/
    testdrawrects();
    HAL_Delay(1000);
    /*画圆测试*/
    testfillcircles(15);
    HAL_Delay(1000);
    /*字体测试*/
    tftPrintTest();
}

/*画线测试*/
void testlines(void) 
{
    LCD_Clear(WHITE);
    POINT_COLOR=BLACK;
    for (int16_t x=0; x < SSD_VER_RESOLUTION; x+=6) 
    {
        LCD_DrawLine(0, 0, x, SSD_HOR_RESOLUTION-1);
        HAL_Delay(3);
    }
    for (int16_t y=0; y < SSD_HOR_RESOLUTION; y+=6) 
    {
        LCD_DrawLine(0, 0, SSD_VER_RESOLUTION-1, y);
        HAL_Delay(3);
    }

    LCD_Clear(WHITE);
    POINT_COLOR=BLUE;
    for (int16_t x=0; x < SSD_VER_RESOLUTION; x+=6)
    {
        LCD_DrawLine(SSD_VER_RESOLUTION-1, 0, x, SSD_HOR_RESOLUTION-1);
        HAL_Delay(3);
    }
    for (int16_t y=0; y < SSD_HOR_RESOLUTION; y+=6)
    {
        LCD_DrawLine(SSD_VER_RESOLUTION-1, 0, 0, y);
        HAL_Delay(3);
    }

    LCD_Clear(WHITE);
    POINT_COLOR=RED;
    for (int16_t y=0; y < SSD_HOR_RESOLUTION; y+=6) 
    {
        LCD_DrawLine(0, SSD_HOR_RESOLUTION-1, SSD_VER_RESOLUTION-1, y);
        HAL_Delay(3);
    }
    for (int16_t x=0; x < SSD_VER_RESOLUTION; x+=6) 
    {
        LCD_DrawLine(0, SSD_HOR_RESOLUTION-1, x, 0);
        HAL_Delay(3);
    }

    LCD_Clear(WHITE);
    POINT_COLOR=MAGENTA;
    for (int16_t x=0; x < SSD_VER_RESOLUTION; x+=6) 
    {
        LCD_DrawLine(SSD_VER_RESOLUTION-1, SSD_HOR_RESOLUTION-1, x, 0);
        HAL_Delay(3);
    }
    for (int16_t y=0; y < SSD_HOR_RESOLUTION; y+=6)
    {
        LCD_DrawLine(SSD_VER_RESOLUTION-1, SSD_HOR_RESOLUTION-1, 0, y);
        HAL_Delay(3);
    }
}

/*画正方形测试*/
void testdrawrects(void) 
{
    LCD_Clear(WHITE);
    POINT_COLOR=BLACK;
    for (int16_t x=0; x < SSD_VER_RESOLUTION; x+=6)
    {
        LCD_DrawRectangle(SSD_VER_RESOLUTION/2 -x/2, SSD_HOR_RESOLUTION/2 -x/2 , x, x);
    }
}

/*画圆测试*/
void testfillcircles(uint8_t radius) 
{
    LCD_Clear(WHITE);
    POINT_COLOR=BLUE;
    for(int16_t x=radius; x < SSD_VER_RESOLUTION; x+=radius*2)
    {
        for(int16_t y=radius; y < SSD_HOR_RESOLUTION; y+=radius*2)
        {
            LCD_Draw_Circle(x, y, radius);
        }
    }
    HAL_Delay(1500);
    LCD_Clear(WHITE);
    POINT_COLOR=MAGENTA;
    radius = radius / 2;
    for(int16_t x=radius; x < SSD_VER_RESOLUTION; x+=radius*2)
    {
        for(int16_t y=radius; y < SSD_HOR_RESOLUTION; y+=radius*2)
        {
            LCD_Draw_Circle(x, y, radius);
        }
    }
}

/*字体测试*/
void tftPrintTest(void) 
{
    LCD_Clear(WHITE);
    LCD_SetCursor(0, 30);
    POINT_COLOR=RED;
    LCD_ShowString(0,30,200,16,16,(uint8_t *)"Hello World!");
    POINT_COLOR=YELLOW;
    LCD_ShowString(0,60,200,16,24,(uint8_t *)"Hello World!");
    POINT_COLOR=GREEN;
    LCD_ShowString(0,90,200,16,32,(uint8_t *)"Hello World!");
    POINT_COLOR=BLUE;
    LCD_ShowxNum(0,120,1234,4,24,1);
    HAL_Delay(1500);
    POINT_COLOR=YELLOW;
    LCD_ShowString(0,180,200,16,24,(uint8_t *)"Hello World!");
    POINT_COLOR=GREEN;
    LCD_ShowString(0,210,200,16,32,(uint8_t *)" Want pi?");
    LCD_ShowString(0,240,200,16,32,(uint8_t *)" Print HEX!");
    POINT_COLOR=BLUE;
    LCD_ShowString(0,270,200,16,16,(uint8_t *)"Sketch has been");
    LCD_ShowString(0,300,200,16,24,(uint8_t *)"running for: ");
    POINT_COLOR=MAGENTA;
    LCD_ShowString(0,330,200,16,24,(uint8_t *)" seconds.");
}

在main.c中声明这些用于测试的函数

void LCD_TEST(void);
void testlines(void);
void testdrawrects(void);
void testfillcircles(uint8_t radius);
void tftPrintTest(void);

最后在主函数中对LCD初始化,在主循环中周期调用测试函数即可,具体源代码如下所示

/*主循环外程序*/
//这个延时在FSMC初始化之后LCD初始化之前,必须要有
HAL_Delay(50);
LCD_Init();

/*主循环内程序*/
LCD_TEST();
HAL_Delay(4000);

4、烧录验证

烧录程序,开发板上电后,LCD会依次测试画线、画矩形、画圆和字符输出功能,具体现象如下图所示

5、注释详解

注释1:图片来源 STM32F4xx 中文参考手册 RM009

注释2:图片来源 https://github.com/hampussandberg/HexConnect/wiki/LCD-ER_TFTM070_5

注释3:图片来源 STM32Cube高效开发教程(基础篇)

注释4:NT35510数据手册来源 https://www.waveshare.com/w/upload/a/a8/NT35510.pdf

注释5:IL9341数据手册来源 https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf

注释6:NT35510芯片LCD驱动程序来自正点原子

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OSnotes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值