STM32CubeMX | 32-使用硬件FMC驱动TFT-LCD屏幕(MCU屏)

   日期:2020-08-30     浏览:144    评论:0    
核心提示:本篇详细的记录了如何使用STM32CubeMX配置 STM32F767IGT6 的硬件FMC外设驱动TFT-LCD屏幕。1. 准备工作硬件准备开发板首先需要准备一个开发板,这里我准备的是STM32F767IGT6的核心板+底板。TFT-LCD开发板底板接正点原子4.3寸TFT-LCD。2. STM32 FMC外设概述2.1. 什么是FMCFMC全称Flexible Memory Controller,灵活的内存控制器,顾名思义,其主要作用是:负责向外部扩展的存储类设备提供控

本篇详细的记录了如何使用STM32CubeMX配置 STM32F767IGT6 的硬件FMC外设驱动TFT-LCD屏幕。

1. 准备工作

硬件准备

  • 开发板
    首先需要准备一个开发板,这里我准备的是STM32F767IGT6的核心板+底板。

  • TFT-LCD
    开发板底板接正点原子4.3寸TFT-LCD。

2. STM32 FMC外设概述

2.1. 什么是FMC

FMC全称Flexible Memory Controller,灵活的内存控制器,顾名思义,其主要作用是:负责向外部扩展的存储类设备提供控制信号

FMC内存控制器支持的存储设备有:

  • Nor Flash、SRAM、PSRAM
  • Nand Flash
  • SDRAM
  • 网卡DM9000(类存储设备)

此外,FMC外设还可以通过配置与LCD控制器连接,它提供Intel 8080并口模式和Motorola 6080并口模式,并且可以灵活的配置为指定的LCD接口类型。

2.2. FMC外设的功能框图

2.3. 外部设备的地址映射(重点)

从FMC的角度来看,外部的存储设备被分为几个固定大小的Bank,每个bank 256 MB

整个FMC外设映射地址的划分如图:

2.3.1. Bank1

BANK1子分区配置

Bank1的地址空间为:0x6000 0000 - 0x6FFF FFFF,支持外接Nor Flash、PSRAM、SRAM等设备,还可以外接DM9000等类存储设备。

整个Bank1的地址空间被划分为四个子bank,每个子bank的大小为64MB,刚好对应FMC外设的地址总线(FMC_A[0:25])有26条(2^26=64MB)。

FMC还有两条内部总线ADDR[27:26],用这两路控制片选信号,如下表:

综合一下如下表:

BANK1控制时序模型

接下来讲述BANK1控制外部存储器的时序模式,BANK1又称为Nor Flash/SRAM/PSRAM控制器,后续暂且叫它SRAM控制器。

SRAM控制器支持两种控制模式:

  • 同步模式
  • 异步模式

对于异步模式,FMC主要设置三个时序参数:

  • 地址建立时间:ADDSET
  • 数据建立时间:DATASET
  • 地址保持时间:ADDHLD

根据SRAM、PSRAM、Nor Flash的综合特点,FMC定义了四种不同的异步时序模型,如下表:

本文中控制TFT-LCD使用的就是异步ModeA时序模型

异步ModeA时序模型

模式A时序模型的优势在于:支持独立的读写时序控制。这一点对于控制TFT-LCD来说,非常符合。因为TFT-LCD在读的时候,一般比较慢,而在写入的时候一般比较快。

模式A的读操作时序如图:

模式A的写操作时序如图:

图中ADDSET和DATASET两个时序的值,后续配置的时候会详细讲述。

2.3.2. Bank3

只能外接Nand Flash设备。

2.3.3. SDRAM Bank

只能外接SDRAM设备。

3. 使用STM32CubeMX生成工程

选择芯片型号

打开STM32CubeMX,打开MCU选择器:

搜索并选中芯片STM32F767IGT6:

配置时钟源

  • 如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC;
  • 如果使用默认内部时钟(HSI),这一步可以略过;

这里我都使用外部时钟:

配置串口

开发板板载了一个CH340换串口,连接到USART1,但是引脚不是默认引脚,需要手动修改。

接下来开始配置USART1并修改引脚:

配置FMC外设

本文所使用的开发板中,将TFT-LCD当做SRAM来操作,连接在FMC的BANK1的第一个区域。
知识点:为什么TFT-LCD可以当做SRAM来控制?
因为TFT-LCD和SRAM相比,同样需要D0-D15数据线,WR、RD、CS控制线,唯一不同的就是TFT-LCD需要一条RS信号线(用于控制传输的是命令还是数据),而SRAM则需要一堆地址线,所以可以巧妙的使用任意一条地址线来当做RS信号。

FMC接口基本配置

开发板上TFT-LCD接口如图:

该接口的原理图如下:

通过原理图可以看出:

  • LCD D0-D15:使用了16bit:FMC D0 - FMC D15;
  • LCD_RS:使用FMC A18来控制向LCD写入数据还是命令(0-命令,1-数据);
  • LCD_BL:背光控制,对应PB5;
  • LCD_CS:LCD片选信号,对应PD7,FMC_NE1;
  • LCD_WR :LCD写使能,对应PD5,FMC_NWE;
  • LCD_RD:LCD读使能,对应PD4,FMC_NOE;
  • RESET:LCD复位信号,直接与单片机复位信号接在一起;

根据这些信息,在STM32CubeMX中先配置BANK1第一个分区的基本设置:

SRAM时序参数配置

首先设置基本的参数,允许读与写使用不同的模式:

接着配置读操作和写操作时序:

其中主要的时序参数配置方法如下:

① HCLK

时序参数都是以HCLK的周期为单位的,在本文中HCLK=216Mhz,所以一个周期为4.63ns。

② 地址建立时间:Address setup time(ADDSET)

该时序的最大值的15个HCLK,也就是15x4.63=69.45ns,一般设为最大值就好。

③ 数据持续时间:Data setup time(DATASET,读时序特有)

该时序的最大值为255个HCLK,为了让驱动程序兼容更多的屏幕,一般设置为85个HCLK即可吗,大约为368ns。

配置额外的GPIO

在查看TFT-LCD原理图的时候,除了与FMC外设相连的引脚外,还有一些其它的控制引脚,比如LCD背光控制:

配置时钟树

STM32F767IG的最高主频到216M,使HCLK = 216Mhz即可:

生成工程设置

代码生成设置

最后设置生成独立的初始化文件:

生成代码

点击GENERATE CODE即可生成MDK-V5工程:

4. 编写TFT-LCD驱动(测试是否可以正常读写ID)

封装底层发送/读取函数

LCD的底层无非就是两个API:发送命令、发送数据,(有的还需要从屏幕读取数据),读取接下来使用FMC外设的 HAL 库API,封装出这两个底层API。

之前查看原理图的时候,表示命令或者数据的LCD_RS控制引脚接在FMC_A18上,也就是说地址数据的第18位,所以在头文件lcd-fmc.h中先定义如下的数据类型:

typedef struct lcd_fmc_address_st {
    uint16_t lcd_reg;
    uint16_t lcd_ram;
} lcd_fmc_address_t;

然后定义一个全局变量指针指向LCD的操作地址,巧妙的让硬件控制所有信号:


#define LCD_BASE ((uint32_t)(0x60000000 | 0x0007FFFE))
#define LCD ((lcd_fmc_address_t*)LCD_BASE)

接着开始封装两个(三个)底层操作函数:

① 发送命令函数:

static void lcd_write_cmd(volatile uint16_t cmd)
{
    cmd = cmd;      //make compiler happy
    LCD->lcd_reg = cmd;
}

② 发送数据函数:

static void lcd_write_data(volatile uint16_t data)
{
    data = data;    //make compiler happy
    LCD->lcd_ram = data;
}

③ 读取数据函数:

static uint16_t lcd_read_data(void)
{
    volatile uint16_t data;
    
    data = LCD->lcd_ram;
    
    return data;
}

基于这三个底层API,还可以封装出读写LCD内部寄存器的函数,为了减少一层函数栈调用,我们不调用之前封装的函数,直接操作:

static void lcd_write_reg(uint16_t reg, uint16_t data)
{
    LCD->lcd_reg = reg;
    LCD->lcd_ram = data;
}

static uint16_t lcd_read_reg(uint16_t reg)
{
    uint16_t data;
    
    LCD->lcd_reg = reg;
    HAL_Delay(1);
    data = LCD->lcd_ram;
    
    return data;
}

MPU配置函数

首先在头文件lcd_fmc.h中定义MPU保护参数:


#define LCD_REGION_NUMBER MPU_REGION_NUMBER0 //LCD使用region0
#define LCD_ADDRESS_START (0x60000000) //LCD区的首地址
#define LCD_REGION_SIZE MPU_REGION_SIZE_256MB //LCD区大小

然后在lcd_fmc.c中编写MPU配置函数:

static void lcd_mpu_config(void)
{	
	MPU_Region_InitTypeDef MPU_Initure;

    
	HAL_MPU_Disable();
    
	
	MPU_Initure.Enable=MPU_REGION_ENABLE;
	MPU_Initure.Number=LCD_REGION_NUMBER;
	MPU_Initure.BaseAddress=LCD_ADDRESS_START;
	MPU_Initure.Size=LCD_REGION_SIZE;
	MPU_Initure.SubRegionDisable=0x00;
	MPU_Initure.TypeExtField=MPU_TEX_LEVEL0;
	MPU_Initure.AccessPermission=MPU_REGION_FULL_ACCESS;
	MPU_Initure.DisableExec=MPU_INSTRUCTION_ACCESS_ENABLE;
	MPU_Initure.IsShareable=MPU_ACCESS_NOT_SHAREABLE;
	MPU_Initure.IsCacheable=MPU_ACCESS_NOT_CACHEABLE;
	MPU_Initure.IsBufferable=MPU_ACCESS_BUFFERABLE;
	HAL_MPU_ConfigRegion(&MPU_Initure);
    
    
	HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

LCD控制参数结构体

为了方便驱动不同的IC,保存不同的控制参数,在lcd_fmc.h中封装如下数据类型:


typedef struct lcd_params_st {
    uint16_t lcd_width;
    uint16_t lcd_height;
    uint16_t lcd_id;
    uint8_t  lcd_direction;
    uint16_t wram_cmd;
    uint16_t set_x_cmd;
    uint16_t set_y_cmd;
} lcd_params_t;

然后在头文件中声明外部变量定义,方便其他程序访问:

extern lcd_params_t lcd_params;

lcd_fmc.c中定义此变量为全局变量:

lcd_params_t lcd_params;

LCD驱动打印日志的处理

为了方便程序开发,难免要打印一些日志,但是如果printf没有被重定向,则会导致LCD驱动卡死。为了避免这个问题,我们使用宏开关的方式来控制是否打印。

lcd_fmc.h中定义此宏开关:


#define LCD_LOG_ENABLE 1

接着可以定义一个日志打印函数:

#if LCD_LOG_ENABLE
#include <stdio.h>
#define LCD_LOG printf
#else
#define LCD_LOG(format,...)
#endif

之后所以需要打印的地方使用LCD_LOG代替printf即可。

编写LCD控制器ID读取函数

通过主动读取此控制器ID,可以自动检测出是哪种类型的控制器,然后执行不同的驱动代码:

static int lcd_read_id(void)
{
    
    lcd_write_cmd(0xD3);				   
	lcd_params.lcd_id = lcd_read_data();
	lcd_params.lcd_id = lcd_read_data();
	lcd_params.lcd_id = lcd_read_data();				   
	lcd_params.lcd_id <<= 8;
	lcd_params.lcd_id |= lcd_read_data();
    
    if (lcd_params.lcd_id == 0x9341) {
        return 0;
    }
    
    
    lcd_write_cmd(0xD4);				   
    lcd_params.lcd_id = lcd_read_data();
    lcd_params.lcd_id = lcd_read_data();
    lcd_params.lcd_id = lcd_read_data();
    lcd_params.lcd_id <<= 8;	 
    lcd_params.lcd_id |= lcd_read_data();
    
    if (lcd_params.lcd_id == 0x5310) {
        return 0;
    }
    
    
    lcd_write_cmd(0xDA00);	
    lcd_params.lcd_id = lcd_read_data();
    lcd_write_cmd(0xDB00);	
    lcd_params.lcd_id = lcd_read_data();
    lcd_params.lcd_id <<= 8;	 
    lcd_write_cmd(0xDC00);	
    lcd_params.lcd_id |= lcd_read_data();
    
    if (lcd_params.lcd_id == 0x8000) {
        lcd_params.lcd_id = 0x5510;
        return 0;
    }
   
    
    lcd_params.lcd_id = 0;
    return -1;
}

编写LCD初始化函数

LCD初始化需要发送大量的命令和数据,本文限于篇幅,只给出读LCD 控制IC的ID的部分,用来测试LCD是否能正常读写足矣。

void lcd_init(void)
{ 	
    
    MX_FMC_Init();
    
    
    HAL_GPIO_WritePin(LCD_BL_GPIO_Port, LCD_BL_Pin, GPIO_PIN_SET);
    
    
    lcd_mpu_config();
	
 	HAL_Delay(50); 
	
 	
    if (lcd_read_id() == -1) {
        LCD_LOG("Not Support LCD IC!\r\n");
        return;
    } else {
        LCD_LOG("LCD IC ID is:%#x\r\n", lcd_params.lcd_id);  
    }
	
	return;
}


lcd_fmc.h中声明该函数:

void lcd_init(void);

测试是否可以正常操作LCD

main.c中包含进来头文件:



#include <stdio.h>
#include "lcd_fmc.h"

然后在man函数中调用:


printf("4.3' TFT-LCD Driver Test By Mculover666\r\n");
lcd_init();

编译,下载,在串口助手中查看结果:

更多精彩文章及资源,请关注我的微信公众号:『mculover666』

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服