历史上的今天
返回首页

历史上的今天

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

正在发生

2021年07月26日 | STM32CubeMX | 35-使用硬件FSMC驱动TFT-LCD屏幕

2021-07-26 来源:eefocus

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


1. 准备工作

硬件准备

  • 开发板
    首先需要准备一个开发板,这里我准备的是STM32F407ZGT6的开发板。

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

2. STM32 FSMC外设概述

2.1. 什么是FSMC

FSMC全称 Flexible static memory controller,灵活的静态内存控制器,顾名思义,其主要作用是:负责向外部扩展的存储类设备提供控制信号。


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

  • Nor Flash、SRAM、PSRAM

  • Nand Flash

  • 类SRAM设备

2.2. FSMC外设的功能框图

在这里插入图片描述

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

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

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

2.3.1. Bank1

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


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


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

BANK1控制时序模型

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


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

  • 同步模式

  • 异步模式

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

  • 地址建立时间:ADDSET

  • 数据建立时间:DATASET

  • 地址保持时间:ADDHLD

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

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


异步ModeA时序模型

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

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

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

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


2.3.2. Bank2、3/4

只能外接Nand Flash设备和PC Card设备:

3. 使用STM32CubeMX生成工程

选择芯片型号

打开STM32CubeMX,打开MCU选择器:

搜索并选中芯片STM32F407ZGT6:

配置时钟源

  • 如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC;

  • 如果使用默认内部时钟(HSI),这一步可以略过;

这里我都使用外部时钟:

调试选项配置

默认没有配置下载引脚,烧录之后下载器将无法再检测到,这里我使用ST-Link,所以配置为SW选项:

配置串口

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


接下来开始配置USART1:

配置FSMC外设

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

FSMC配置

开发板上 TFT-LCD 的原理图如下:

通过原理图可以看出:

  • LCD D0-D15:使用了16bit:FSMC D0 - FSMC D15;

  • LCD_RS:使用FSMC A6来控制向LCD写入数据还是命令(0-命令,1-数据);

  • LCD_BL:背光控制,对应PB5;

  • LCD_CS:LCD片选信号,FMC_NE4,表示使用Bank1的Bank4子区域

  • LCD_WR :LCD写使能,FSMC_NWE;

  • LCD_RD:LCD读使能,FSMC_NOE;

  • RESET:LCD复位信号,直接与单片机复位信号接在一起;

根据这些信息,在STM32CubeMX中先配置SRAM4的基本设置:

此处如果选择LCD接口类型和SRAM类型的区别在于:
LCD接口类型只会配置用到的那一个地址引脚,而SRAM类型则会配置所有的地址引脚。

SRAM基本参数配置

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

SRAM时序参数配置

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

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

读时序配置

① HCLK

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

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

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

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

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

写时序配置

① HCLK

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

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

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

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

写时序比较快,该时序的最大值为255个HCLK,图中可以看出,NT35510控制器要求的数据建立时间最小为15ns,但因为读时序比较慢,所以设为3,3x5.95=17.85ns。

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

配置背光引脚

配置时钟树

STM32F407ZGT6的最高主频到168M,使HCLK = 168Mhz即可:
在这里插入图片描述

生成工程设置

代码生成设置

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

生成代码

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

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

特别提醒:STM32CubeMX生成的工程默认开启了-O3优化,编写的驱动太菜了,会出问题,所以遇到玄学Bug请改为-O0优化!


封装底层发送/读取函数

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


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


/* 通过地址线控制RS引脚 */

#define LCD_CMD_ADDR            0x6c00007E

#define LCD_DAT_ADDR            0x6c000080


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


① 发送命令函数:


/**

 * @brief    向LCD写入命令

 * @param    cmd 待写入命令

 * @retval   none

*/

static void lcd_write_cmd(__IO uint16_t cmd)

{

    *(uint16_t *)(LCD_CMD_ADDR) = cmd;

}


② 发送数据函数:


/**

 * @brief    向LCD写入数据

 * @param    data 待写入数据

 * @retval   none

*/

static void lcd_write_data(__IO uint16_t data)

{

    *(uint16_t *)(LCD_DAT_ADDR) = data;

}


③ 读取数据函数:


/**

 * @brief    从LCD读取数据

 * @param    none

 * @retval   读取到的数据

*/

static uint16_t lcd_read_data(void)

{

    __IO uint16_t data;

    

    data = *(uint16_t *)(LCD_DAT_ADDR);

    

    return data;

}


基于这三个底层API,还可以封装出读写LCD内部寄存器的函数:


/**

 * @brief    写LCD中的寄存器

 * @param    reg  寄存器序号

 * @param    data 要写入寄存器的值

 * @retval   none

*/

static void lcd_write_reg(__IO uint16_t reg, __IO uint16_t data)

{

    lcd_write_cmd(reg);

    lcd_write_data(data);

}


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_fsmc.c中定义此变量为全局变量:


lcd_params_t lcd_params;


LCD驱动打印日志的处理

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


在lcd_fsmc.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_FSMC_Init();

    

    /* 开启背光 */

    HAL_GPIO_WritePin(LCD_BL_GPIO_Port, LCD_BL_Pin, GPIO_PIN_SET);

    

  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_fsmc.h中声明该函数:


void lcd_init(void);


测试是否可以正常操作LCD

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


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

/* USER CODE BEGIN Includes */

#include

#include "lcd_fsmc.h"

/* USER CODE END Includes */


然后在man函数中调用:


/* USER CODE BEGIN 2 */

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

lcd_init();

/* USER CODE END 2 */


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


5. 编写TFT-LCD驱动(初始化、刷屏测试)

可以正常读取ID之后,接下来的工作是:


发送一堆一堆的命令,初始化屏幕;

设置坐标

清屏

刷屏测试

① LCD开显示、关显示、LCD设置扫描方向、 LCD设置显示方向、LCD设置光标位置这些函数代码不多,需要的话请查看源码。


② 清屏函数:



static void lcd_write_ram_start(void)

{

    lcd_write_cmd(lcd_params.wram_cmd);

}


static void lcd_write_ram(uint16_t rgb_color)

{

    lcd_write_data(rgb_color);

}


void lcd_clear(uint16_t color)

{

uint32_t index = 0;      

uint32_t totalpoint = lcd_params.lcd_width;

    

    /* 计算得到总点数 */

totalpoint *= lcd_params.lcd_height;

    

    /* 设置光标位置 */

lcd_set_cursor(0x00,0x0000);

    

    /* 开始写入GRAM */

lcd_write_ram_start();

    

    /* 写入数据到GRAM */

for (index = 0; index < totalpoint; index++) {

lcd_write_ram(color);

}

}


③ 初始化函数:代码过长,请查看源码。


这三类函数实现完之后,就可以编写一个如下的刷屏测试函数:


void lcd_auto_clear(uint16_t period_ms)

{

    lcd_clear(BLACK);

    HAL_Delay(period_ms);

    lcd_clear(BLUE);

    HAL_Delay(period_ms);

    lcd_clear(GREEN);

    HAL_Delay(period_ms);

    lcd_clear(GBLUE);

    HAL_Delay(period_ms);

    lcd_clear(CYAN);

    HAL_Delay(period_ms);

    lcd_clear(GRAY);

    HAL_Delay(period_ms);

    lcd_clear(BROWN);

    HAL_Delay(period_ms);

    lcd_clear(RED);

    HAL_Delay(period_ms);

    lcd_clear(BRED);

    HAL_Delay(period_ms);

    lcd_clear(BRRED);

    HAL_Delay(period_ms);

    lcd_clear(YELLOW);

    HAL_Delay(period_ms);

    lcd_clear(WHITE);

    HAL_Delay(period_ms);

}


在main函数中调用此函数,分别给予不同的刷新频率,测试刷屏速度和效果。


6. 实现打点、画线、填充函数(重点)

打点函数

/**

 * @brief    LCD打点函数

 * @param    x_pos x方向坐标

 * @param    y_pos y方向坐标

 * @retval   none

*/

void lcd_draw_point(uint16_t x_pos, uint16_t y_pos, uint16_t color)

推荐阅读

史海拾趣

DACHANG公司的发展小趣事

随着公司实力的不断增强,DACHANG公司开始积极拓展国际市场。公司参加了多个国际电子展览会,与国际同行进行了广泛的交流与合作。同时,DACHANG公司还积极寻求与国际知名企业的合作机会,不断提升自己在全球电子产业链中的地位和影响力。

Edsun Laboratories Inc公司的发展小趣事

在竞争激烈的电子行业中,品质是企业生存和发展的关键。ECM Electronics Limited.深知这一点,始终将产品质量放在首位。公司建立了严格的质量管理体系,从原材料采购到产品生产、检测、包装等各个环节都严格把关,确保每一件产品都符合高标准的质量要求。正是凭借过硬的产品品质,ECM赢得了客户的信赖和市场的认可。

安国国际(ALCOR)公司的发展小趣事

进入21世纪,安国继续深耕集线器控制芯片领域。2000年,公司发布了首款USB键盘集线器控制芯片——AU9432,这款芯片凭借其卓越的性能和稳定性,成功被Apple的iMAC键盘所采用。这一里程碑事件不仅证明了安国在集线器控制芯片领域的领先地位,也为其后续的产品研发和市场拓展奠定了坚实基础。

AW Industries Inc公司的发展小趣事

除了追求经济效益外,AW Industries Inc还注重履行社会责任。公司积极参与环保事业,通过采用环保材料、减少废弃物排放等方式降低生产对环境的影响。同时,公司还关注员工福利和社会公益事业,通过捐款、资助等方式回馈社会。这些举措不仅提升了公司的社会形象,也为公司的长期发展奠定了坚实的基础。


请注意,这些故事是基于电子行业的一般情况虚构的,并不代表任何真实事件或公司。如果您需要关于特定公司的真实发展故事,建议您查阅相关资料或访问公司官网以获取更准确的信息。

Emmoco公司的发展小趣事

随着产品质量的不断提升和技术的不断创新,Emmoco开始积极拓展市场。公司制定了详细的市场拓展计划,通过参加国际展会、举办技术研讨会等方式,不断提高品牌知名度和影响力。同时,Emmoco还积极与国内外知名企业建立合作关系,共同开拓市场,实现互利共赢。这些举措使得Emmoco的市场份额不断扩大,品牌影响力逐渐增强。

AKM [Asahi Kasei Microsystems]公司的发展小趣事

Emmoco公司成立于XXXX年,由一群富有远见和创新精神的电子工程师创立。公司自创立之初就明确了以技术创新为核心的发展道路,专注于研发高性能的电子元器件和模块。经过初期的不懈努力,Emmoco成功推出了一系列具有竞争力的产品,为公司的后续发展奠定了坚实的基础。

问答坊 | AI 解惑

PolySwitch PPTC器件在汽车電子的应用方案

PPTC器件技术已广泛应用于便携式电器、手机、计算机和远程通信设备的过流和过热电路保护设计中。汽车电子技术委员会推出的有关无源部件的新标准,推动了聚合物正温度系数器件(PPTC)电路保护技术在汽车工业中的应用。 新型汽车设计主要针对电子电 ...…

查看全部问答>

为什么MC52i经常读不到短信?

使用mc52i,每隔半小时通过向服务器传送一个UDP数据包,同时服务器也向mc52i发送短信,但是使用AT+CMGR指令,经常读取不到短信。 有没有做过类似工作的朋友帮帮忙,已经调试了几天了,问题依然存在…

查看全部问答>

做过GRPS数据传输的请进!!!!!!!

第一次做GPRS数据传输,用的是华为的GTM900B。希望各位能给个AT指令建立连接,传输数据的例子。 邮箱:amarantine33520@163.com 不胜感激!!!!!!!…

查看全部问答>

pb找不到当前活动的设备

刚接触wince,我的板子是用cf卡启动的,nk.bin,eboot.bin都可以生成,用eboot启动,用这句启动Loadcepc /v/e:0:11:10.1.12.155 eboot.bin,这个ip和我主机是在一个子网通过路由器连接的 ,启动后目标机会出现\"jumping xxxxx\",网上说这时pb但是target->c ...…

查看全部问答>

嵌入式项目合作(北京)

希望在北京,对下面要做的有经验比较精通。可以合作,也可以外包。 要求为下面的应用选择合适的开发板和器件,ARM内核,linux系统,以太网支持有线和无线,红外和遥控器双向传输。 linux系统上有httpd(通过浏览器对设备进行设置)/dhcpcd(当有 ...…

查看全部问答>

请教大家一个关于光耦电路问题

本信息来自合作QQ群:电子工程师技术交流(12425841) 群主: 蓝天白云请教大家个问题,这个光耦电路,右边输入信号是个开漏电路,最大的灌电流为1mA,低电平最高0.95V,要达到二极管上的电流13mA,R13和R14的阻值怎么计算啊 …

查看全部问答>

PWM控制中常用的算法,PID算法。

PWM控制中常用的算法,PID算法,飞思卡尔智能车大赛的就需要仔细研究这个算法的。我寝室隔壁铺的同学,这个月15号要去长沙参加华南区的比赛了,现在正在实验室整天研究这个算法。  童鞋们,可以仔细读读 [ 本帖最后由 江汉大学南瓜 于 2011-7- ...…

查看全部问答>

怎么在AD转换中断里写SD卡?

我用的是TA的触发信号,然后想在AD转换的中断里把转换的数据写进SD卡的文件里,但是总是只能写一次的数据,不能接着上次写的数据写。文件系统用的FAT16格式的。贴一段代码,大家看看是哪里出问题了。         #inc ...…

查看全部问答>

STM32 如何从新定向printf到串口 (ST例程)

/**   ******************************************************************************   * @file    USART/Printf/main.c   * @author  MCD Application Team   * @version V3.5 ...…

查看全部问答>

只有一个定时器的430,能否输出PWM的同时还能定时中断干其他活?

只有一个定时器的430,能否输出PWM的同时还能定时中断干其他活? 比如系统输出30K的PWM波形,同时还能有0.5秒的定时中断去执行其它任务。 大大能否贴个简单的代码指导一下啊?…

查看全部问答>