STM32学习之使用TFTLCD

前言

终于到了代码部分, 前边的所有知识铺垫都是为了这部分的代码,能得心应手,其实我个人认为,原子哥的驱动代码已经写的很好很完善了,所以这部分主要是学习原子哥的代码怎么用,我手里刚好也有一个 原子哥 的LED屏幕,也是用FSMC控制TFTLCD,就按照原子哥的教程一步一步来了。

学习资料来自:STM32F407最小系统板开发指南-库函数版本_V1.1.pdf
正点原子,感谢原子哥的开源奉献
正点原子资料下载中心

硬件:
STM32F407ZGT6
2.8 LCD MODULE
一个摄像头

  1. 注意:
    共分为3篇:
  2. STM32学习之TFTLCD
  3. STM32学习之FSMC
  4. STM32学习之使用TFTLCD

如果仅仅想要实现,可以直接去看最后一篇的使用,前边的基础知识可以跳过
如果不想看代码解释,直接调到最后一部分直接看应用即可

STM32单片机学习资料均来自 正点原子 ,仅用于学习,如有侵权请联系我删除
本博客内容原创,创作不易,转载请注明

本文链接
个人博客:https://ronglin.fun/?p=105
PDF链接:见博客网站
CSDN: https://blog.csdn.net/RongLin02/article/details/121446125

硬件

这里我们介绍一下 TFTLCD 模块与 ALIETEK STM32F407最小系统板的连接, ALIENTEK TFTLCD 模块接口关系如图 16.2.1 所示:
在这里插入图片描述
图 16.2.1 中圈出来的部分就是连接 TFTLCD 模块的接口,在硬件上,TFTLCD 模块与STM32F407 最小系统板的 IO 口对应关系如下:

  • LCD_BL(背光控制)对应 PB0;
  • LCD_CS 对应 PG12 即 FSMC_NE4;
  • LCD _RS 对应 PF12 即 FSMC_A6;
  • LCD _WR 对应 PD5 即 FSMC_NWE;
  • LCD _RD 对应 PD4 即 FSMC_NOE;
  • LCD _D[15:0]则直接连接在 FSMC_D15~FSMC_D0;
    STM32F407 最小系统板通过 FPC 排线及转接板和 TFTLCD 模块连接。

软件讲解

接下来就是代码讲解了
打开下载好的资料中的 TFTLCD 显示实验工程
可以看到我们添加了两个文件 lcd.c 和头文件 lcd.h
同时,FSMC 相关的库函数分布在 stm32f4xx_fsmc.c 文件和头文件 stm32f4xx_fsmc.h 中。所以我们在工程中要引入 stm32f4xx_fsmc.c 源文件。
在 lcd.c 里面代码比较多,只针对几个重要的函数进行讲解。

基础数据结构

本实验,我们用到 FSMC 驱动 LCD,通过前面的介绍,我们知道 TFTLCD 的 RS 接在 FSMC的 A6 上面,CS 接在 FSMC_NE4 上,并且是 16 位数据总线。即我们使用的是 FSMC 存储器 1的第 4 区,我们定义如下 LCD 操作结构体(在 lcd.h 里面定义):

//LCD 操作结构体
typedef struct
{
    vu16 LCD_REG;
    vu16 LCD_RAM;
} LCD_TypeDef;
//使用 NOR/SRAM 的 Bank1.sector4,地址位 HADDR[27,26]=11 A6 作为数据命令区分线
//注意 16 位数据总线时,STM32 内部地址会右移一位对齐!
#define LCD_BASE ((u32)(0x6C000000 | 0x0000007E))
#define LCD ((LCD_TypeDef *) LCD_BASE)

其中 LCD_BASE,必须根据我们外部电路的连接来确定,我们使用 Bank1.sector4 就是从地址 0X6C000000 开始,而 0X0000007E,则是 A6 的偏移量,这里很多朋友不理解这个偏移量的概念,简单说明下:
以 A6 为例,7E 转换成二进制就是:1111110,而 16 位数据时,地址右移一位对齐,那么实际对应到地址引脚的时候,就是:A6:A0=0111111,此时 A6 是 0,但是如果 16 位地址再加 1(注意:对应到 8 位地址是加 2,即 7E+0X02),那么:A6:A0=1000000,此时 A6 就是 1 了,即实现了对 RS 的 0 和 1 的控制。我们将这个地址强制转换为 LCD_TypeDef 结构体地址,那么可以得到 LCD->LCD_REG 的地址就是 0X6C00,007E,对应 A6 的状态为 0(即 RS=0),而 LCD-> LCD_RAM 的地址就是0X6C00,0080(结构体地址自增),对应 A6 的状态为 1(即 RS=1)。所以,有了这个定义,当我们要往 LCD 写命令/数据的时候,可以这样写:

LCD->LCD_REG=CMD; //写命令
LCD->LCD_RAM=DATA; //写数据

而读的时候反过来操作就可以了,如下所示:

CMD= LCD->LCD_REG;//读 LCD 寄存器
DATA = LCD->LCD_RAM;//读 LCD 数据

这其中,CS、WR、RD 和 IO 口方向都是由 FSMC 控制,不需要我们手动设置了。接下来,
我们先介绍一下 lcd.h 里面的另一个重要结构体:

//LCD 重要参数集
typedef struct
{
    u16 width; //LCD 宽度
    u16 height; //LCD 高度
    u16 id; //LCD ID
    u8 dir; //横屏还是竖屏控制:0,竖屏;1,横屏。
    u16 wramcmd; //开始写 gram 指令
    u16 setxcmd; //设置 x 坐标指令
    u16 setycmd; //设置 y 坐标指令
}_lcd_dev;
//LCD 参数
extern _lcd_dev lcddev; //管理 LCD 重要参数

该结构体用于保存一些 LCD 重要参数信息,比如 LCD 的长宽、LCD ID(驱动 IC 型号)、LCD 横竖屏状态等,这个结构体虽然占用了十几个字节的内存,但是却可以让我们的驱动函数支持不同尺寸的 LCD,同时可以实现 LCD 横竖屏切换等重要功能,所以还是利大于弊的。
有了以上了解,下面我们开始介绍 lcd.c 里面的一些重要函数。先看 7 个简单,但是很重要的函数:

7个基础函数

//regval:寄存器值
void LCD_WR_REG(vu16 regval)
{
	regval=regval;		//使用-O2优化的时候,必须插入的延时
	LCD->LCD_REG=regval;//写入要写的寄存器序号
}
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(vu16 data)
{
	data=data;			//使用-O2优化的时候,必须插入的延时
	LCD->LCD_RAM=data;
}
//读LCD数据
//返回值:读到的值
u16 LCD_RD_DATA(void)
{
	vu16 ram;			//防止被优化
	ram=LCD->LCD_RAM;
	return ram;
}
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue)
{
	LCD->LCD_REG = LCD_Reg;		//写入要写的寄存器序号
	LCD->LCD_RAM = LCD_RegValue;//写入数据
}
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(u16 LCD_Reg)
{
	LCD_WR_REG(LCD_Reg);		//写入要读的寄存器序号
	delay_us(5);
	return LCD_RD_DATA();		//返回读到的值
}
//开始写GRAM
void LCD_WriteRAM_Prepare(void)
{
 	LCD->LCD_REG=lcddev.wramcmd;
}
//LCD写GRAM
//RGB_Code:颜色值
void LCD_WriteRAM(u16 RGB_Code)
{
	LCD->LCD_RAM = RGB_Code;//写十六位GRAM
}

因为 FSMC 自动控制了WR/RD/CS 等这些信号,所以这 7 个函数实现起来都非常简单,我们就不多说,注意,上面有几个函数,我们添加了一些对 MDK –O2 优化的支持,去掉的话,在-O2 优化的时候会出问题。这些函数实现功能见函数前面的备注,通过这几个简单函数的组合,我们就可以对 LCD 进行各种操作了。

常用函数

void LCD_SetCursor(u16 Xpos, u16 Ypos)

第七个要介绍的函数是坐标设置函数,该函数代码如下:

//设置光标位置
//Xpos:横坐标
//Ypos:纵坐标
void LCD_SetCursor(u16 Xpos, u16 Ypos)
{
 	if(lcddev.id==0X9341||lcddev.id==0X5310)
	{
		LCD_WR_REG(lcddev.setxcmd);
		LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
		LCD_WR_REG(lcddev.setycmd);
		LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF);
	}else if(lcddev.id==0X6804)
	{
		if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏时处理
		LCD_WR_REG(lcddev.setxcmd);
		LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
		LCD_WR_REG(lcddev.setycmd);
		LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF);
	}else if(lcddev.id==0X1963)
	{
		if(lcddev.dir==0)//x坐标需要变换
		{
			Xpos=lcddev.width-1-Xpos;
			LCD_WR_REG(lcddev.setxcmd);
			LCD_WR_DATA(0);LCD_WR_DATA(0);
			LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
		}else
		{
			LCD_WR_REG(lcddev.setxcmd);
			LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
			LCD_WR_DATA((lcddev.width-1)>>8);LCD_WR_DATA((lcddev.width-1)&0XFF);
		}
		LCD_WR_REG(lcddev.setycmd);
		LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF);
		LCD_WR_DATA((lcddev.height-1)>>8);LCD_WR_DATA((lcddev.height-1)&0XFF);

	}else if(lcddev.id==0X5510)
	{
		LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(Xpos>>8);
		LCD_WR_REG(lcddev.setxcmd+1);LCD_WR_DATA(Xpos&0XFF);
		LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(Ypos>>8);
		LCD_WR_REG(lcddev.setycmd+1);LCD_WR_DATA(Ypos&0XFF);
	}else
	{
		if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏其实就是调转x,y坐标
		LCD_WriteReg(lcddev.setxcmd, Xpos);
		LCD_WriteReg(lcddev.setycmd, Ypos);
	}
}

该函数实现将 LCD 的当前操作点设置到指定坐标(x,y)。因为 9341/5310/6804/5510 等的设置同其他屏有些不太一样,所以进行了区别对待。

void LCD_DrawPoint(u16 x,u16 y)

接下来我们介绍第八个函数:画点函数。该函数实现代码如下:

//画点
//x,y:坐标
//POINT_COLOR:此点的颜色
void LCD_DrawPoint(u16 x,u16 y)
{
	LCD_SetCursor(x,y);		//设置光标位置
	LCD_WriteRAM_Prepare();	//开始写入GRAM
	LCD->LCD_RAM=POINT_COLOR;
}

该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。其中 POINT_COLOR 是我们定义的一个全局变量,用于存放画笔颜色,顺带介绍一下另外一个全局变量:BACK_COLOR该变量代表 LCD 的背景色。
LCD_DrawPoint 函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。

u16 LCD_ReadPoint(u16 x,u16 y)

有了画点,当然还需要有读点的函数,第九个介绍的函数就是读点函数,用于读取 LCD的 GRAM,这里说明一下,为什么 OLED 模块没做读 GRAM 的函数,而这里做了。因为 OLED 模块是单色的,所需要全部 GRAM 也就 1K 个字节,而 TFTLCD 模块为彩色的,点数也比 OLED 模块多很多,以 16 位色计算,一款 320×240 的液晶,需要 320×240×2 个字节来存储颜色值,也就是也需要 150K 字节,这对任何一款单片机来说,都不是一个小数目了。而且我们在图形叠加的时候,可以先读回原来的值,然后写入新的值,在完成叠加后,我们又恢复原来的值。这样在做一些简单菜单的时候,是很有用的。这里我们读取 TFTLCD 模块数据的函数为 LCD_ReadPoint,该函数直接返回读到的 GRAM 值。该函数使用之前要先设置读取的 GRAM地址,通过 LCD_SetCursor 函数来实现。LCD_ReadPoint 的代码如下:

//读取个某点的颜色值
//x,y:坐标
//返回值:此点的颜色
u16 LCD_ReadPoint(u16 x,u16 y)
{
 	u16 r=0,g=0,b=0;
	if(x>=lcddev.width||y>=lcddev.height)return 0;	//超过了范围,直接返回
	LCD_SetCursor(x,y);
	if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310||lcddev.id==0X1963)LCD_WR_REG(0X2E);//9341/6804/3510/1963 发送读GRAM指令
	else if(lcddev.id==0X5510)LCD_WR_REG(0X2E00);	//5510 发送读GRAM指令
	else LCD_WR_REG(0X22);      		 			//其他IC发送读GRAM指令
	if(lcddev.id==0X9320)opt_delay(2);				//FOR 9320,延时2us
 	r=LCD_RD_DATA();								//dummy Read
	if(lcddev.id==0X1963)return r;					//1963直接读就可以
	opt_delay(2);
 	r=LCD_RD_DATA();  		  						//实际坐标颜色
 	if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)		//9341/NT35310/NT35510要分2次读出
 	{
		opt_delay(2);
		b=LCD_RD_DATA();
		g=r&0XFF;		//对于9341/5310/5510,第一次读取的是RG的值,R在前,G在后,各占8位
		g<<=8;
	}
	if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0XB505||lcddev.id==0XC505)return r;	//这几种IC直接返回颜色值
	else if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)return (((r>>11)<<11)|((g>>10)<<5)|(b>>11));//ILI9341/NT35310/NT35510需要公式转换一下
	else return LCD_BGR2RGB(r);						//其他IC
}

LCD_ReadPoint 函数中,因为我们的代码不止支持一种 LCD 驱动器,所以,我们根据不同的 LCD 驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数的通用性。

void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode)

第十个要介绍的是字符显示函数 LCD_ShowChar,该函数同前面 OLED 模块的字符显示函数差不多,但是这里的字符显示函数多了 1 个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。
该函数实现代码如下:

//在指定位置显示一个字符
//x,y:起始坐标
//num:要显示的字符:" "--->"~"
//size:字体大小 12/16/24
//mode:叠加方式(1)还是非叠加方式(0)
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode)
{
    u8 temp,t1,t;
	u16 y0=y;
	u8 csize=(size/8+((size%8)?1:0))*(size/2);		//得到字体一个字符对应点阵集所占的字节数
 	num=num-' ';//得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库)
	for(t=0;t<csize;t++)
	{
		if(size==12)temp=asc2_1206[num][t]; 	 	//调用1206字体
		else if(size==16)temp=asc2_1608[num][t];	//调用1608字体
		else if(size==24)temp=asc2_2412[num][t];	//调用2412字体
		else return;								//没有的字库
		for(t1=0;t1<8;t1++)
		{
			if(temp&0x80)LCD_Fast_DrawPoint(x,y,POINT_COLOR);
			else if(mode==0)LCD_Fast_DrawPoint(x,y,BACK_COLOR);
			temp<<=1;
			y++;
			if(y>=lcddev.height)return;		//超区域了
			if((y-y0)==size)
			{
				y=y0;
				x++;
				if(x>=lcddev.width)return;	//超区域了
				break;
			}
		}
	}
}

LCD_ShowChar 函数里面,我们采用快速画点函数 LCD_Fast_DrawPoint 来画点显示字符,该函数同 LCD_DrawPoint 一样,只是带了颜色参数,且减少了函数调用的时间,详见本例程源码。该代码中我们用到了三个字符集点阵数据数组 asc2_2412、asc2_1206 和 asc2_1608,
这几个字符集的点阵数据的提取方式,同十七章介绍的提取方法是一模一样的。详细请参考第
十七章。

void LCD_Init(void)

最后,我们再介绍一下 TFTLCD 模块的初始化函数 LCD_Init,该函数先初始化 STM32 与 TFTLCD 连接的 IO 口,并配置 FSMC 控制器,然后读取 LCD 控制器的型号,根据控制 IC 的型号执行不同的初始化代码,代码过多,不在再贴出
从初始化代码可以看出,LCD 初始化步骤为①~⑥在代码中标注:

  • ① GPIO,FSMC 使能。
  • ② GPIO 初始化:GPIO_Init()函数。
  • ③ 设置引脚复用映射。
  • ④ FSMC 初始化:FSMC_NORSRAMInit()函数。
  • ⑤ FSMC 使能:FSMC_NORSRAMCmd()函数。
  • ⑥ 不同的 LCD 驱动器的初始化代码。
    该函数先对 FSMC 相关 IO 进行初始化,然后是 FSMC 的初始化,这个我们在前面都有介绍,最后根据读到的 LCD ID,对不同的驱动器执行不同的初始化代码,从上面的代码可以看出,这个初始化函数可以针对十多款不同的驱动 IC 执行初始化操作,这样大大提高了整个程序的通用性。大家在以后的学习中应该多使用这样的方式,以提高程序的通用性、兼容性。

特别注意:本函数使用了 printf 来打印 LCD ID,所以,如果你在主函数里面没有初始化串口,那么将导致程序死在 printf 里面!!如果不想用 printf,那么请注释掉它。

常用函数总结

此部分类似于API,如果想要仔细了解,请见上边的软件详解部分

lcd.h文件

void LCD_Init(void);													   	//初始化
void LCD_DisplayOn(void);													//开显示
void LCD_DisplayOff(void);													//关显示
void LCD_Clear(u16 Color);	 												//清屏
void LCD_SetCursor(u16 Xpos, u16 Ypos);										//设置光标
void LCD_DrawPoint(u16 x,u16 y);											//画点
void LCD_Fast_DrawPoint(u16 x,u16 y,u16 color);								//快速画点
u16  LCD_ReadPoint(u16 x,u16 y); 											//读点
void LCD_Draw_Circle(u16 x0,u16 y0,u8 r);						 			//画圆
void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2);							//画线
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2);		   				//画矩形
void LCD_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 color);		   				//填充单色
void LCD_Color_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 *color);				//填充指定颜色
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode);						//显示一个字符
void LCD_ShowNum(u16 x,u16 y,u32 num,u8 len,u8 size);  						//显示一个数字
void LCD_ShowxNum(u16 x,u16 y,u32 num,u8 len,u8 size,u8 mode);				//显示 数字
void LCD_ShowString(u16 x,u16 y,u16 width,u16 height,u8 size,u8 *p);		//显示一个字符串,12/16字体

void LCD_WriteReg(u16 LCD_Reg, u16 LCD_RegValue);
u16 LCD_ReadReg(u16 LCD_Reg);
void LCD_WriteRAM_Prepare(void);
void LCD_WriteRAM(u16 RGB_Code);
void LCD_SSD_BackLightSet(u8 pwm);							//SSD1963 背光控制
void LCD_Scan_Dir(u8 dir);									//设置屏扫描方向
void LCD_Display_Dir(u8 dir);								//设置屏幕显示方向
void LCD_Set_Window(u16 sx,u16 sy,u16 width,u16 height);	//设置窗口

main函数示例

LCD 驱动相关的函数就给大家讲解到这里。接下来,我们看看主函数代码如下:

int main(void)
{
    u8 x=0;
    u8 lcd_id[12]; //存放 LCD ID 字符串
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组 2
    delay_init(168); //初始化延时函数
    uart_init(115200); //初始化串口波特率为 115200
    LED_Init(); //初始化 LED
    LCD_Init(); //初始化 LCD FSMC 接口
    POINT_COLOR=RED;
    sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id);//将 LCD ID 打印到 lcd_id 数组。
 while(1)
{
    switch(x)
    {
        case 0:LCD_Clear(WHITE);break;
        case 1:LCD_Clear(BLACK);break;
        case 2:LCD_Clear(BLUE);break;
        case 3:LCD_Clear(RED);break;
        case 4:LCD_Clear(MAGENTA);break;
        case 5:LCD_Clear(GREEN);break;
        case 6:LCD_Clear(CYAN);break;
        case 7:LCD_Clear(YELLOW);break;
        case 8:LCD_Clear(BRRED);break;
        case 9:LCD_Clear(GRAY);break;
        case 10:LCD_Clear(LGRAY);break;
        case 11:LCD_Clear(BROWN);break;
    }
    POINT_COLOR=RED;
    LCD_ShowString(30,40,210,24,24,"Explorer STM32F4");
    LCD_ShowString(30,70,200,16,16,"TFTLCD TEST");
    LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
    LCD_ShowString(30,110,200,16,16,lcd_id); //显示 LCD ID
    LCD_ShowString(30,130,200,12,12,"2014/5/4");
     x++;
    if(x==12)x=0;
    LED0=!LED0;delay_ms(1000);
 }
}

该部分代码将显示一些固定的字符,字体大小包括24*12、16*8 和 12*6等三种,同时显示LCD 驱动 IC 的型号,然后不停的切换背景颜色,每 1s 切换一次。而 LED0 也会不停的闪烁,指示程序已经在运行了。其中我们用到一个 sprintf 的函数,该函数用法同 printf,只是 sprintf把打印内容输出到指定的内存区间上,sprintf 的详细用法,请百度。
另外特别注意:uart_init 函数,不能去掉,因为在 LCD_Init 函数里面调用了 printf,所以一旦你去掉这个初始化,就会死机了!实际上,只要你的代码有用到 printf,就必须初始化串口,否则都会死机,即停在 usart.c 里面的 fputc 函数,出不来。

在编译通过之后,我们开始下载验证代码。

最后经过板载测试,代码正确且运行正常,学到了学到了,终于点亮了这块屏幕。=w=

  • 5
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值