软件设计我们依旧在之前的工程上面增加,首先在HARDWARE文件夹下新建一个LCD的文件夹。然后打开USER文件夹下的工程,新建一个ILI93xx.c的文件和lcd.h的头文件,保存在LCD文件夹下,并将LCD文件夹加入头文件包含路径。
在ILI93xx.c里面要输入的代码比较多,我们这里就不贴出来了,只针对几个重要的函数进行讲解。完整版的代码见光盘à4,程序源码à标准例程à实验13 TFTLCD显示实验的ILI93xx.c文件。
本实验,我们用到FSMC驱动LCD,通过前面的介绍,我们知道TFTLCD的RS接在FSMC的A10上面,CS接在FSMC_NE4上,并且是16位数据总线。即我们使用的是FSMC存储器1的第4区,我们定义如下LCD操作结构体(在lcd.h里面定义):
//LCD操作结构体
typedef struct
{
u16
LCD_REG;
u16
LCD_RAM;
} LCD_TypeDef;
//使用NOR/SRAM的 Bank1.sector4,地址位HADDR[27,26]=11 A10作为数据命令区分线
//注意16位数据总线时,STM32内部地址会右移一位对齐!
#define LCD_BASE ((u32)(0x6C000000 | 0x000007FE))
#define LCD ((LCD_TypeDef *) LCD_BASE)
其中LCD_BASE,必须根据我们外部电路的连接来确定,我们使用Bank1.sector4就是从地址0X6C000000开始,而0X000007FE,则是A10的偏移量。我们将这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的地址就是0X6C00,07FE,对应A10的状态为0(即RS=0),而LCD-> LCD_RAM的地址就是0X6C00,0800(结构体地址自增),对应A10的状态为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,横屏。
u8 wramcmd; //开始写gram指令
u8 setxcmd; //设置x坐标指令
u8 setycmd; //设置y坐标指令
}_lcd_dev;
//LCD参数
extern _lcd_dev lcddev; //管理LCD重要参数
该结构体用于保存一些LCD重要参数信息,比如LCD的长宽、LCD ID(驱动IC型号)、LCD横竖屏状态等,这个结构体虽然占用了10个字节的内存,但是却可以让我们的驱动函数支持不同尺寸的LCD,同时可以实现LCD横竖屏切换等重要功能,所以还是利大于弊的。有了以上了解,下面我们开始介绍ILI93xx.c里面的一些重要函数。
先看6个简单,但是很重要的函数:
//写寄存器函数
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{
LCD->LCD_REG=regval;//写入要写的寄存器序号
}
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(u16 data)
{
LCD->LCD_RAM=data;
}
//读LCD数据
//返回值:读到的值
u16 LCD_RD_DATA(void)
{
return
LCD->LCD_RAM;
}
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue)
{
LCD->LCD_REG
= LCD_Reg; //写入要写的寄存器序号
LCD->LCD_RAM
= LCD_RegValue;//写入数据
}
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(u8 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;
}
因为FSMC自动控制了WR/RD/CS等这些信号,所以这6个函数实现起来都非常简单,我们就不多说,实现功能见函数前面的备注,通过这几个简单函数的组合,我们就可以对LCD进行各种操作了。
第七个要介绍的函数是坐标设置函数,该函数代码如下:
//设置光标位置
//Xpos:横坐标
//Ypos:纵坐标
void LCD_SetCursor(u16 Xpos, u16 Ypos)
{
if(lcddev.id==0X9341)
{
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.dir==1)Xpos=lcddev.width-1-Xpos;//横屏其实就是调转x,y坐标
LCD_WriteReg(lcddev.setxcmd,
Xpos);
LCD_WriteReg(lcddev.setycmd,
Ypos);
}
}
该函数实现将LCD的当前操作点设置到指定坐标(x,y)。因为9341的设置同其他屏有些不太一样,所以单独对9341进行了设置。
接下来我们介绍第八个函数:画点函数。该函数实现代码如下:
//画点
//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函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。
有了画点,当然还需要有读点的函数,第九个介绍的函数就是读点函数,用于读取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)LCD_WR_REG(0X2E); //9341 发送读GRAM指令
else
LCD_WR_REG(R34); //其他IC发送读GRAM指令
if(lcddev.id==0X9320)opt_delay(2); //FOR 9320,延时2us
if(LCD->LCD_RAM)r=0; //dummy
Read
opt_delay(2);
r=LCD->LCD_RAM; //实际坐标颜色
if(lcddev.id==0X9341)//9341要分2次读出
{
opt_delay(2);
b=LCD->LCD_RAM;
g=r&0XFF;//对于9341,第一次读取的是RG的值,R在前,G在后,各占8位
g<<=8;
}
//这几种IC直接返回颜色值
if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0X8989||
lcddev.id==0XB505)return r;
else if(lcddev.id==0X9341)return
(((r>>11)<<11)|((g>>10)<<5)|(b>>11));
//ILI9341需要公式转换一下
else return LCD_BGR2RGB(r); //其他IC
}
在LCD_ReadPoint函数中,因为我们的代码不止支持一种LCD驱动器,所以,我们根据不同的LCD驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数的通用性。
第十个要介绍的是字符显示函数LCD_ShowChar,该函数同前面OLED模块的字符显示函数差不多,但是这里的字符显示函数多了1个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。该函数实现代码如下:
//在指定位置显示一个字符
//x,y:起始坐标
//num:要显示的字符:"
"--->"~"
//size:字体大小 12/16
//mode:叠加方式(1)还是非叠加方式(0)
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8
mode)
{
u8 temp,t1,t;
u16
y0=y;
u16
colortemp=POINT_COLOR;
//设置窗口
num=num-'
';//得到偏移后的值
if(!mode)
//非叠加方式
{
for(t=0;t<size;t++)
{
if(size==12)temp=asc2_1206[num][t]; //调用1206字体
else
temp=asc2_1608[num][t]; //调用1608字体
for(t1=0;t1<8;t1++)
{
if(temp&0x80)POINT_COLOR=colortemp;
else
POINT_COLOR=BACK_COLOR;
LCD_DrawPoint(x,y);
temp<<=1;
y++;
if(x>=lcddev.height)
{POINT_COLOR=colortemp;return;}//超区域了
if((y-y0)==size)
{
y=y0;
x++;
if(x>=lcddev.width)
{POINT_COLOR=colortemp;return;}//超区域了
break;
}
}
}
}else//叠加方式
{
for(t=0;t<size;t++)
{
if(size==12)temp=asc2_1206[num][t]; //调用1206字体
else
temp=asc2_1608[num][t]; //调用1608字体
for(t1=0;t1<8;t1++)
{
if(temp&0x80)LCD_DrawPoint(x,y);
temp<<=1;
y++;
if(x>=lcddev.height)
{POINT_COLOR=colortemp;return;}//超区域了
if((y-y0)==size)
{
y=y0;
x++;
if(x>=lcddev.width)
{POINT_COLOR=colortemp;return;}//超区域了
break;
}
}
}
}
POINT_COLOR=colortemp;
}
在LCD_ShowChar函数里面,我们采用画点函数来显示字符,虽然速度不如开辟窗口的方式,但是这样写可以有更好的兼容性,方便在不同LCD之间移植。该代码中我们用到了两个字符集点阵数据数组asc2_1206和asc2_1608,这两个字符集的点阵数据的提取方式,同十七章介绍的提取方法是一模一样的。详细请参考第十七章。
最后,我们再介绍一下TFTLCD模块的初始化函数LCD_Init,该函数先初始化STM32与TFTLCD连接的IO口,并配置FSMC控制器,然后读取LCD控制器的型号,根据控制IC的型号执行不同的初始化代码,其简化代码如下:
//初始化lcd
//该初始化函数可以初始化各种ILI93XX液晶,但是其他函数是基于ILI9320的!!!
//在其他型号的驱动芯片上没有测试!
void LCD_Init(void)
{
RCC->AHBENR|=1<<8; //使能FSMC时钟
RCC->APB2ENR|=1<<3; //使能PORTB时钟
RCC->APB2ENR|=1<<5; //使能PORTD时钟
RCC->APB2ENR|=1<<6; //使能PORTE时钟
RCC->APB2ENR|=1<<8; //使能PORTG时钟
RCC->APB2ENR|=1<<0; //使能AFIO时钟
GPIOB->CRL&=0XFFFFFFF0;//PB0推挽输出 背光
GPIOB->CRL|=0X00000003;
//PORTD复用推挽输出
GPIOD->CRH&=0X00FFF000;
GPIOD->CRH|=0XBB000BBB;
GPIOD->CRL&=0XFF00FF00;
GPIOD->CRL|=0X00BB00BB;
//PORTE复用推挽输出
GPIOE->CRH&=0X00000000;
GPIOE->CRH|=0XBBBBBBBB;
GPIOE->CRL&=0X0FFFFFFF;
GPIOE->CRL|=0XB0000000;
//PORTG12复用推挽输出 A0
GPIOG->CRH&=0XFFF0FFFF;
GPIOG->CRH|=0X000B0000;
GPIOG->CRL&=0XFFFFFFF0;//PG0->RS
GPIOG->CRL|=0X0000000B;
//寄存器清零
//bank1有NE1~4,每一个有一个BCR+TCR,所以总共八个寄存器。
//这里我们使用NE4 ,也就对应BTCR[6],[7]。
FSMC_Bank1->BTCR[6]=0X00000000;
FSMC_Bank1->BTCR[7]=0X00000000;
FSMC_Bank1E->BWTR[6]=0X00000000;
//操作BCR寄存器 使用异步模式
FSMC_Bank1->BTCR[6]|=1<<12; //存储器写使能
FSMC_Bank1->BTCR[6]|=1<<14; //读写使用不同的时序
FSMC_Bank1->BTCR[6]|=1<<4; //存储器数据宽度为16bit
//操作BTR寄存器
//读时序控制寄存器
FSMC_Bank1->BTCR[7]|=0<<28; //模式A
FSMC_Bank1->BTCR[7]|=1<<0; //地址建立时间(ADDSET)为2个HCLK
//因为液晶驱动IC的读数据的时候,速度不能太快,尤其对1289这个IC。
FSMC_Bank1->BTCR[7]|=0XF<<8; //数据保存时间为16个HCLK
//写时序控制寄存器
FSMC_Bank1E->BWTR[6]|=0<<28; //模式A
FSMC_Bank1E->BWTR[6]|=0<<0; //地址建立时间(ADDSET)为1个HCLK
//4个HCLK(HCLK=72M)因为液晶驱动IC的写信号脉宽,
//最少也得50ns。72M/4=24M=55ns
FSMC_Bank1E->BWTR[6]|=3<<8; //数据保存时间为4个HCLK
//使能BANK1,区域4
FSMC_Bank1->BTCR[6]|=1<<0; //使能BANK1,区域4
delay_ms(50);
// delay 50 ms
LCD_WriteReg(0x0000,0x0001);
delay_ms(50);
// delay 50 ms
lcddev.id = LCD_ReadReg(0x0000);
if(lcddev.id <0XFF||lcddev.id==0XFFFF)//读到ID不正确
{
//尝试9341的ID读取
LCD_WR_REG(0XD3);
LCD_RD_DATA(); //dummy read
LCD_RD_DATA(); //读到0X00
lcddev.id=LCD_RD_DATA(); //读取93
lcddev.id<<=8;
lcddev.id|=LCD_RD_DATA(); //读取41
}
printf(" LCD
ID:%x\r\n",lcddev.id); //打印LCD ID
if(lcddev.id==0X9341) //9341初始化
{
……//9341初始化代码
}else
if(lcddev.id==0x9325)//9325
{
……//9325初始化代码
}else if(lcddev.id==0x9328) //ILI9328 OK
{
……//9328初始化代码
}else
if(lcddev.id==0x9320||lcddev.id==0x9300)//未测试.
{
……//9300初始化代码
}else
if(lcddev.id==0X9331)
{
……//9331初始化代码
}else
if(lcddev.id==0x5408)
{
……//5408初始化代码
}
else
if(lcddev.id==0x1505)//OK
{
……//1505初始化代码
}else
if(lcddev.id==0xB505)
{
……//B505初始化代码
}else
if(lcddev.id==0xC505)
{
……//C505初始化代码
}else
if(lcddev.id==0x8989)
{
……//8989初始化代码
}else
if(lcddev.id==0x4531)
{
……//4531初始化代码
}else
if(lcddev.id==0x4535)
{
……//4535初始化代码
}
LCD_Display_Dir(0); //默认为竖屏显示
LCD_LED=1; //点亮背光
LCD_Clear(WHITE);
}
该函数先对FSMC相关IO进行初始化,然后是FSMC的初始化,这个我们在前面都有介绍,最后根据读到的LCD ID,对不同的驱动器执行不同的初始化代码,从上面的代码可以看出,这个初始化函数可以针对13款不同的驱动IC执行初始化操作,这样大大提高了整个程序的通用性。大家在以后的学习中应该多使用这样的方式,以提高程序的通用性、兼容性。
特别注意:本函数使用了printf来打印LCD ID,所以,如果你在主函数里面没有初始化串口,那么将导致程序死在printf里面!!如果不想用printf,那么请注释掉它。
保存ILI93xx.c,并将该代码加入到HARDWARE组下。在介绍完了ILI93xx.c的内容之后,然后我们在lcd.h里面输入如下内容:
#ifndef __LCD_H
#define __LCD_H
#include "sys.h"
#include "stdlib.h"
//LCD重要参数集
typedef struct
{
u16
width; //LCD 宽度
u16
height; //LCD 高度
u16
id; //LCD ID
u8 dir; //横屏还是竖屏控制:0,竖屏;1,横屏。
u8 wramcmd; //开始写gram指令
u8 setxcmd; //设置x坐标指令
u8 setycmd; //设置y坐标指令
}_lcd_dev;
//LCD参数
extern _lcd_dev lcddev; //管理LCD重要参数
//LCD的画笔颜色和背景色
extern u16 POINT_COLOR;//默认红色
extern u16 BACK_COLOR; //背景颜色.默认为白色
//////////////////////////////////////////////////////////////////////////////////
//-----------------LCD端口定义----------------
#define LCD_LED
PBout(0) //LCD背光 PB0
//LCD地址结构体
typedef struct
{
u16
LCD_REG;
u16
LCD_RAM;
} LCD_TypeDef;
//使用NOR/SRAM的 BANK 4,地址位HADDR[27,26]=11 A10作为数据命令区分线
//注意设置时STM32内部会右移一位对其! 111110=0X3E
#define LCD_BASE ((u32)(0x6C000000 | 0x000007FE))
#define LCD ((LCD_TypeDef *) LCD_BASE)
//////////////////////////////////////////////////////////////////////////////////
//扫描方向定义
#define L2R_U2D 0 //从左到右,从上到下
#define L2R_D2U 1 //从左到右,从下到上
#define R2L_U2D 2 //从右到左,从上到下
#define R2L_D2U 3 //从右到左,从下到上
#define U2D_L2R 4 //从上到下,从左到右
#define U2D_R2L 5 //从上到下,从右到左
#define D2U_L2R 6 //从下到上,从左到右
#define D2U_R2L 7 //从下到上,从右到左
#define DFT_SCAN_DIR L2R_U2D //默认的扫描方向
//画笔颜色
#define WHITE 0xFFFF
……//省略部分
#define LBBLUE 0X2B12 //浅棕蓝色(选择条目的反色)
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); //画点
u16 LCD_ReadPoint(u16 x,u16 y); //读点
void 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(u8 LCD_Reg, u16 LCD_RegValue);
u16 LCD_ReadReg(u8 LCD_Reg);
void LCD_WriteRAM_Prepare(void);
void LCD_WriteRAM(u16 RGB_Code);
void LCD_Scan_Dir(u8 dir); //设置屏扫描方向
void LCD_Display_Dir(u8 dir); //设置屏幕显示方向
//9320/9325 LCD寄存器
#define R0 0x00
……//省略部分
#define R229 0xE5
#endif
这段代码的两个重要结构体定义,我们都在前面有介绍,其他的相对就比较简单了。另外这段代码对颜色和驱动器的寄存器进行了很多宏定义,限于篇幅考虑,我们没有完全贴出来,省略了其中绝大部分。此部分我们就不多说了。接下来,我们在test.c里面修改main函数如下:
int main(void)
{
u8 x=0;
u8
lcd_id[12]; //存放LCD ID字符串
Stm32_Clock_Init(9); //系统时钟设置
uart_init(72,9600); //串口初始化为9600
delay_init(72); //延时初始化
LED_Init(); //初始化与LED连接的硬件接口
LCD_Init();
POINT_COLOR=RED;
sprintf((char*)lcd_id,"LCD
ID:%04X",lcddev.id);//将LCD ID打印到lcd_id数组
while(1)
{
switch(x)
{
case
0 CD_Clear(WHITE);break;
case
1 CD_Clear(BLACK);break;
case
2 CD_Clear(BLUE);break;
case
3 CD_Clear(RED);break;
case
4 CD_Clear(MAGENTA);break;
case
5 CD_Clear(GREEN);break;
case
6 CD_Clear(CYAN);break;
case 7 CD_Clear(YELLOW);break;
case
8 CD_Clear(BRRED);break;
case
9 CD_Clear(GRAY);break;
case
10:LCD_Clear(LGRAY);break;
case
11:LCD_Clear(BROWN);break;
}
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"WarShip
STM32 ^_^");
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,16,16,"2012/9/5");
x++;
if(x==12)x=0;
LED0=!LED0;
delay_ms(1000);
}
}
该部分代码将显示一些固定的字符,同时显示LCD驱动IC的型号,然后不停的切换背景颜色,每1s切换一次。而LED0也会不停的闪烁,指示程序已经在运行了。其中我们用到一个sprintf的函数,该函数用法同printf,只是sprintf把打印内容输出到指定的内存区间上,sprintf的详细用法,请百度。
在编译通过之后,我们开始下载验证代码。
将程序下载到战舰STM32后,可以看到DS0不停的闪烁,提示程序已经在运行了。同时可以看到TFTLCD模块的显示如图18.4.1所示:
图18.4.1 TFTLCD显示效果图
我们可以看到屏幕的背景是不停切换的,同时DS0不停的闪烁,证明我们的代码被正确的执行了,达到了我们预期的目的。
|