历史上的今天
返回首页

历史上的今天

今天是:2025年07月26日(星期六)

正在发生

2021年07月26日 | STM32CubeMX | 32-使用硬件FMC驱动TFT-LCD屏幕(MCU屏)

2021-07-26 来源:eefocus

本篇详细的记录了如何使用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时序参数配置

本文中使用的LCD控制器为NT35510控制器,找到其数据手册,查看:

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

读时序配置

① HCLK

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

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

该时序的最大值的15个HCLK,从图中可以看出,NT35110控制器要求读的时候最小为10ns,,所以设为3即可,3x4.63=13.89ns。

③ 数据持续时间:Data setup time(DATASET)

读时序比较慢,该时序的最大值为255个HCLK,从图中可以看出,NT35510控制器要求的数据建立时间最小为15ns,但因为读时序比较慢,所以设为5,5x4.63=23.15ns。

写时序配置

① HCLK

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

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

该时序的最大值为15个HCLK,NT35110控制器要求写的时候最小为0,,所以设为0即可。

③ 数据持续时间:Data setup time(DATASET)

写时序比较快,该时序的最大值为255个HCLK,图中可以看出,NT35510控制器要求的数据建立时间最小为15ns,所以设为4,4x4.63=18.52ns。

综合上述计算,配置如下:

配置额外的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的操作地址,巧妙的让硬件控制所有信号:


/*

    作用:

    - 操作 LCD->lcd_reg 地址处的数据,则FMC外设发出信号时,A18=0,即LCD_RS=0

    - 操作 LCD->lcd_ram 地址处的数据,则FMC外设发出信号时,A18=1,即LCD_RS=1

*/

#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保护参数:


/* LCD 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;


    /* 配置之前先关闭MPU */

HAL_MPU_Disable();

    

/* 配置MPU */

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);

    

    /* 开启MPU */

HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

}


LCD控制参数结构体

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


/**

 * @brief    保存LCD屏幕参数

 * @param    lcd_width     LCD屏幕宽度

 * @param    lcd_height    LCD屏幕高度

 * @param    lcd_id        LCD 驱动IC ID

 * @param    lcd_direction LCD横屏显示还是竖屏显示,0-竖屏,1-横屏

 * @param    wram_cmd      开始写gram指令

 * @param    set_x_cmd     设置x坐标指令

 * @param    set_y_cmd     设置y坐标指令

*/

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中定义此宏开关:


/* 使能此驱动是否打印调试日志(需要printf支持) */

#define LCD_LOG_ENABLE          1


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


#if LCD_LOG_ENABLE

#include

#define LCD_LOG printf

#else

#define LCD_LOG(format,...)

#endif


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


编写LCD控制器ID读取函数

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


static int lcd_read_id(void)

{

    /* 尝试执行ILI9341控制器ID的读取流程 */

    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;

    }

    

    /* 尝试执行NT35310控制器ID的读取流程 */

    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;

    }

    

    /* 尝试执行NT35510控制器ID的读取流程 */

    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;

    }

   

    /* 驱动IC不支持 */

    lcd_params.lcd_id = 0;

    return -1;

}


编写LCD初始化函数

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


void lcd_init(void)

{

    /* 初始化FMC接口 */

    MX_FMC_Init();

    

    /* 开启背光 */

    HAL_GPIO_WritePin(LCD_BL_GPIO_Port, LCD_BL_Pin, GPIO_PIN_SET);

    

    /* 配置MPU */

    lcd_mpu_config();

  HAL_Delay(50); 

  /* 读取LCD控制器IC */

    if (lcd_read_id() == -1) {

        LCD_LOG("Not Support LCD IC!rn");

        return;

    } else {

        LCD_LOG("LCD IC ID is:%#xrn", lcd_params.lcd_id);  

    }

return;

}


在lcd_fmc.h中声明该函数:


void lcd_init(void);

1.

测试是否可以正常操作LCD

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


/* Private includes ----------------------------------------------------------*/

/* USER CODE BEGIN Includes */

#include

#include "lcd_fmc.h"

/* USER CODE END Includes */


然后在man函数中调用:


/* USER CODE BEGIN 2 */

printf("4.3' TFT-LCD Driver Test By Mculover666rn");

lcd_init();

/* USER CODE END 2 */


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

推荐阅读

史海拾趣

巨盛电子(Chesen)公司的发展小趣事

随着国内市场的饱和,巨盛电子(Chesen)开始寻求国际化发展。公司制定了一系列国际化战略,包括拓展海外市场、建立海外生产基地、与国际品牌合作等。通过这些努力,巨盛电子成功进入了欧美、东南亚等市场,实现了品牌的全球化布局。同时,公司还积极与国际品牌合作,共同开发新产品,提高了品牌知名度和市场份额。

CT Micro公司的发展小趣事
  1. 创业初期与技术创新

CT Micro公司最初由几位电子工程领域的专家创立,他们看到了微型计算机断层扫描(Micro-CT)技术在电子行业中的巨大潜力。初期,公司面临着资金短缺和技术难题,但他们通过不断研发和创新,成功开发出了一款具有高性价比的Micro-CT设备,迅速获得了市场的认可。

  1. 市场拓展与合作伙伴关系

随着产品的成熟,CT Micro开始积极寻求市场拓展。他们与多家电子制造企业建立了合作关系,为这些企业提供Micro-CT设备的定制服务。通过与这些企业的合作,CT Micro不仅扩大了市场份额,还进一步提升了产品的技术水平和应用范围。

  1. 研发升级与产品迭代

面对日益激烈的市场竞争,CT Micro不断投入研发力量,对Micro-CT设备进行升级和迭代。他们成功推出了多款新型设备,具有更高的分辨率、更快的扫描速度和更低的辐射剂量。这些新产品的推出,进一步巩固了CT Micro在电子行业中的领先地位。

  1. 国际化战略与市场拓展

随着国内市场的饱和,CT Micro开始实施国际化战略。他们积极参与国际展览和研讨会,展示自己的产品和技术实力。同时,他们还在海外设立了销售和服务中心,为国际客户提供更加便捷的服务。通过这些努力,CT Micro成功打开了国际市场的大门。

  1. 社会责任与可持续发展

在快速发展的同时,CT Micro也积极履行社会责任。他们注重环保和可持续发展,采用环保材料和节能技术生产产品。此外,他们还积极参与公益事业,为贫困地区的教育和医疗事业贡献力量。这些举措不仅提升了公司的社会形象,也为其可持续发展奠定了坚实基础。

请注意,这些故事框架是虚构的,并不代表CT Micro公司的实际发展情况。如果您需要了解CT Micro公司或类似公司的真实故事,建议您查阅相关公司的官方网站、新闻报道或行业分析报告。

捷嘉电子(Chequers Electronic)公司的发展小趣事

随着市场需求的不断变化,捷嘉电子意识到只有不断创新才能保持竞争力。于是,公司投入大量资源进行技术研发,特别是在智能控制器PCBA制造及智能产品研发方面取得了显著成果。其中,公司研发的一款新型智能家居控制器,以其卓越的稳定性和易用性受到了市场的热烈欢迎。这款产品的成功,不仅提升了捷嘉电子的品牌知名度,也为其后续发展奠定了坚实的基础。

固得沃克(GOODWORK)公司的发展小趣事

为了进一步提升品牌影响力和市场占有率,固得沃克积极寻求与行业内知名企业的战略合作。例如,公司与锐单商城达成战略合作,共同推动电子元器件的在线销售和服务。这一合作不仅拓宽了公司的销售渠道,还提升了公司的品牌知名度和美誉度。通过不断参与行业交流和合作,固得沃克在电子行业中的影响力日益增强,为公司的长远发展奠定了坚实基础。

DBLECTRO公司的发展小趣事

1997年,迪贝电子在上海正式成立,标志着这家专注于环境感知技术和气体检测及分析领域的创新企业正式进入中国市场。当时,中国的环保法规日益严格,对气体排放的监控和管理要求也不断提高。迪贝电子凭借其在该领域的深厚积累和技术实力,迅速在市场上推出了一系列高质量的气体检测和分析仪器。这些产品以其准确性、稳定性和易用性受到了用户的广泛好评,为迪贝电子在中国市场的快速发展奠定了坚实的基础。

Gespac Inc公司的发展小趣事

2010年,迪贝电子面临了一次重大的市场挑战。随着国内外竞争对手的不断涌入,气体检测及分析仪器市场的竞争日益激烈。为了应对这一挑战,迪贝电子加大了技术创新的力度,积极引进国内外最新的技术和设备,不断提升产品的性能和品质。同时,公司还加强了与客户的沟通和合作,深入了解客户需求,提供个性化的解决方案。这些努力使得迪贝电子在激烈的市场竞争中保持了领先地位。

问答坊 | AI 解惑

150M的虚拟示波器 DSO-2150 USB

DSO-2150 USB 流线型设计,体积小巧, USB2.0接口,免电源,与台式示波器类似界面,易于上手. 更适合于笔记本电脑,生产线维修调整,便于出差使用。 小的尺寸(mm):190(L)x100(W)x35(H) ,便于携带. .高刷新率, 高采样率,150MS/s实时采样. 软 ...…

查看全部问答>

PIC单片机教程 西安电子科技大学

PIC单片机教程      西安电子科技大学 网上找的觉得不错,传上来,让大家一起分享!…

查看全部问答>

大家帮帮忙 程序查错

在keil编译 错误一大堆烦死了,大家帮帮忙看下 哪出问题了 先谢谢了 #include sbit p3_0=P3^0; sbit p0_0=P0^0; sbit p0_1=P0^1; void delay(unsigned int k); int cheak(unsigned char *jj); void scankey(); int n=0; main() {   ...…

查看全部问答>

wince 日文EUC编码

wince帮助中好像只有日文JIS编码没有日文EUC编码。wince日文系统怎样支持日文EUC编码啊?…

查看全部问答>

内核保护以及数字签名

在WINCE 6.0下,内核下的EXE以及DLL都能正常运行,外部数字签名过的EXE以及DLL也可以在这个内核下运行。任何无数字签名的EXE以及DLL则不能运行。不知该如何做? 我在平台定制时,已经将sysgen_certmod设为1。重新build and sysgen。 …

查看全部问答>

request_region()到底如何使用?

谁能举个例子, 比如,我想使用arm芯片的I/O口C,也就是GPGC,用它的若干个口吧,比如8~11 该如何调用这个函数呢?期待答案,谢谢 …

查看全部问答>

51中,cpu根据啥来决定何种寻址方式的?可以举个例子嘛?

51中,cpu根据啥来决定何种寻址方式的?可以举个例子嘛?…

查看全部问答>

msp430f413编译出错

Error[Pe020]: identifier "BCSCTL1" is undefined 头文件:#include<msp430x41x.h> 请帮忙分析下,应该是头文件里没BCSCTL1的定义。如果如此,又应该包含哪个头文件呢。…

查看全部问答>

SPS-2000焊锡搅拌机 (MALCOM)

 SPS-2000焊锡搅拌机(MALCOM)特长:无铅焊锡搅拌时温度等的关系是重要的因素MALCOM <SPS-2000>设定了温度管理、实现搅拌自动停止机能,无论是刚从冰箱拿出的锡膏,工作人员只需按一下按钮就可以搅拌出最佳的状态的锡膏机器。公转约 ...…

查看全部问答>

一个逻辑工程师的成长之路-逻辑工程师水平的V模型

理论篇讲述了一个逻辑工程师是如何发展的,并且给出了各个阶段的特征,但是,仍然没给出清晰的量化,本章,尝试对这个问题给出一个解决方案。逻辑工程师应该掌握的各种能力大概设置了5种能力,每种能力在不同的阶段有不同的要求。其中,设计完成能 ...…

查看全部问答>