本文主要是介绍STM32学习之使用TFTLCD,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
终于到了代码部分, 前边的所有知识铺垫都是为了这部分的代码,能得心应手,其实我个人认为,原子哥的驱动代码已经写的很好很完善了,所以这部分主要是学习原子哥的代码怎么用,我手里刚好也有一个 原子哥 的LED屏幕,也是用FSMC控制TFTLCD,就按照原子哥的教程一步一步来了。
学习资料来自:STM32F407最小系统板开发指南-库函数版本_V1.1.pdf
正点原子,感谢原子哥的开源奉献
正点原子资料下载中心
硬件:
STM32F407ZGT6
2.8 LCD MODULE
一个摄像头
- 注意:
共分为3篇: - STM32学习之TFTLCD
- STM32学习之FSMC
- 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 IDu8 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(); //开始写入GRAMLCD->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,延时2usr=LCD_RD_DATA(); //dummy Readif(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);//设置系统中断优先级分组 2delay_init(168); //初始化延时函数uart_init(115200); //初始化串口波特率为 115200LED_Init(); //初始化 LEDLCD_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 IDLCD_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=
这篇关于STM32学习之使用TFTLCD的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!