STM32F407 或 STM32F417 系列芯片都带有 FSMC 接口,能够与同步或异步存储器和 16 位 PC 存储器卡连接,包括 SRAM、 NAND FLASH、 NOR FLASH 和 PSRAM 等存储器。
STM32F4 的 FSMC 将外部设备分为 2 类: NOR/PSRAM 设备、NAND/PC 卡设备。
共用地址数据总线等信号,他们具有不同的 CS 以区分不同的设备,
TFTLCD 就是用的 FSMC_NE4 做片选,其实就是将 TFTLCD 当成 SRAM 来控制。
为什么可以把 TFTLCD 当成 SRAM 设备用:
外部 SRAM 的控制一般有:地址线(如 A0~A18)、数据线(如 D0~D15)、写信号(WE)、
读信号(OE)、片选信号(CS),如果 SRAM 支持字节控制,那么还有 UB/LB 信号。
TFTLCD的信号包括: RS、 D0-D15、 WR、 RD、 CS、 RST 和 BL 等,其中真正在操作 LCD 的时候需要用到的就只有: RS、 D0-D15、 WR、 RD 和 CS。其操作时序和 SRAM的控制完全类似,唯一不同就是 TFTLCD 有 RS 信号,但是没有地址信号。
RS 信号来决定传送的数据是数据还是命令,本质上可以理解为一个地址信号,比如我们把 RS 接在 A6上面,那么当 FSMC 控制器写地址 0x00-0x3F 的时候,A6 为 0,对 TFTLCD 来说,就是写命令。而 FSMC 写地址线0x40-0x7F的时候, A6 将会变为 1,对 TFTLCD 来说,就是写数据。
FSMC 支持 8/16/32 位数据宽度,我们这里用到的 LCD 是 16 位宽度的,所以在设置的时候,选择 16 位宽就 OK 了。
FSMC 将外部存储器划分为固定大小为 256M 字节的四个存储块,FSMC 总共管理 1GB 空间,拥有 4 个存储块(Bank),
当STM32的内核程序,读写的地址位于某个Bank之中的时候,会自动生成对应的合适的接口时序信号。
例如,
读写地址位于bank1,FSMC会生成PSRAM或者NORFLASH的接口时序信号。
读写地址位于bank2,FSMC会生成NANDFLASH的接口时序信号。
读写地址位于bank3,FSMC会生成NANDFLASH的接口时序信号。
读写地址位于bank4,FSMC会生成NANDFLASH的接口时序信号。
Bank1,对应地址区间是0X6000_0000–0x6FFF_FFFF,
Bank2,对应地址区间是0X7000_0000–0x7FFF_FFFF,
Bank3,对应地址区间是0X8000_0000–0x8FFF_FFFF,
Bank4,对应地址区间是0X9000_0000–0x9FFF_FFFF,
每个bank被分为4个region,每个sector对应一个片选。例如,对于bank1,
Bank1_1,对应输出的片选是FSMC_NE1,地址区间是0X6000_0000–0x63FF_FFFF,
Bank1_2,对应输出的片选是FSMC_NE2,地址区间是0X6400_0000–0x67FF_FFFF,
Bank1_3,对应输出的片选是FSMC_NE3,地址区间是0X6800_0000–0x6BFF_FFFF,
Bank1_4,对应输出的片选是FSMC_NE4,地址区间是0X6C00_0000–0x6FFF_FFFF,
ARM内核是字节寻址的,所以,
当 Bank1 接的是 16 位宽度存储器的时候: HADDR[25:1]→ FSMC_A[24:0]。
当 Bank1 接的是 8 位宽度存储器的时候: HADDR[25:0]→ FSMC_A[25:0]。
++++++++++++++++++++++++++++++++++++++++++++++++++
初始化 FSMC 主要是初始化三个寄存器 FSMC_BCRx, FSMC_BTRx,FSMC_BWTRx, 在 HAL 库中提供了 FSMC 初始化函数为
HAL_StatusTypeDef HAL_SRAM_Init(SRAM_HandleTypeDef *hsram,
FMC_NORSRAM_TimingTypeDef *Timing,
FMC_NORSRAM_TimingTypeDef *ExtTiming)
SRAM_HandleTypeDef 类 型 指 针 变 量,这是SRAM的句柄。
FMC_NORSRAM_TimingTypeDef 类型指针变量,是时序描述符的句柄。
typedef struct
{
uint32_t AddressSetupTime;
uint32_t AddressHoldTime;
uint32_t DataSetupTime;
uint32_t BusTurnAroundDuration;
uint32_t CLKDivision;
uint32_t DataLatency;
uint32_t AccessMode;
}FSMC_NORSRAM_TimingTypeDef;
+++++++++++++++++++++++++++++++++++++++++++++++
TFTLCD 的 RS 接在 FSMC的 A6 上面, CS 接在 FSMC_NE4 上,并且是 16 位数据总线。即我们使用的是 FSMC 的bank 1的sector4,即bank1_4.
我们采用Memory Map的方式,对LCD的读写区域进行访问。
所以需要定义LCD_BASE,
#define LCD_BASE ((u32)(0x6C00007E))
为了方便对LCD的数据读写进行管理,我们定义了一个结构体
typedef struct
{
volatile uint16_t LCD_REG;
volatile uint16_t LCD_RAM;
} LCD_TypeDef;
将REG读写单元和SRAM读写单元整合在一起。
为了方便引用这个MMRegion,
我们将LCD_BASE强转为LCD_TypeDef类型的指针。
#define LCD ((LCD_TypeDef *) LCD_BASE)
当我们要往 LCD 写命令/数据的时候,可以这样写:
LCD->LCD_REG=cmd; //写命令
LCD->LCD_RAM=data; //写数据
cmd = LCD->LCD_REG;//读 LCD 寄存器
data = LCD->LCD_RAM;//读 LCD 数据
在MM的工作方式下,CS、 WR、 RD 和 IO 口方向都是由 FSMC 控制。
这样,就可以封装最基本的读写函数了,
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(vu16 LCD_Reg, vu16 LCD_RegValue)
{
LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号
LCD->LCD_RAM = LCD_RegValue; //写入数据
}
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(vu16 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
}
++++++++++++++++++++++++++++++++++++++
为了对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 重要参数
+++++++++++++++++++++++++++++++++++++++++++
来看看一个调用基本的读写函数实现功能的功能级函数,
//设置光标位置
//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==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)。
更进一步的功能函数,就是调用别的功能级函数。
//画点
//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 的背景色。
读取 TFTLCD 模块数据的函数为LCD_ReadPoint,该函数直接返回读到的 GRAM 值。
//读取个某点的颜色值
//x,y:坐标
//返回值:此点的颜色
u16 LCD_ReadPoint(u16 x,u16 y)
{
vu16 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)
LCD_WR_REG(0X2E);
//9341/6804/3510 发送读 GRAM 指令
else if(lcddev.id==0X5510)
LCD_WR_REG(0X2E00);//5510 发送读 GRAM 指令
else
LCD_WR_REG(R34); //其他 IC 发送读 GRAM 指令
if(lcddev.id==0X9320)
opt_delay(2); //FOR 9320,延时 2us
LCD_RD_DATA(); //dummy Read
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
}
其他几乎所有上层函数,都是通过调用这几个函数实现的。
++++++++++++++++++++++++++++++++++++++++++++
更进一步,我们需要更高层的功能函数,高层功能函数调用低层的功能函数。
//在指定位置显示一个字符
//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-' ';//得到偏移后的值
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的设备参数,并存储在设备参数描述符中。
所以,我们需要设计一个功能级函数,它调用基本的读写函数。
void LCD_Init(void)
{
uint16_t tmp_id;
delay_ms(50); // delay 50 ms
lcddev.id = 0xFFFF;
//尝试 9341 ID 的读取
LCD_WR_REG(0XD3);
tmp_id=LCD_RD_DATA(); //dummy read
tmp_id=LCD_RD_DATA(); //读到 0X00
tmp_id=LCD_RD_DATA(); //读取 93
tmp_id<<=8;
tmp_id|=LCD_RD_DATA(); //读取 41
if(tmp_id == 0x9341)
lcddev.id = 0x9341;
if(lcddev.id ==0XFFFF) //非 9341,尝试看看是不是 NT35310
{
LCD_WR_REG(0XD4);
tmp_id=LCD_RD_DATA();//dummy read
tmp_id=LCD_RD_DATA();//读回 0X01
tmp_id=LCD_RD_DATA();//读回 0X53
tmp_id<<=8;
tmp_id|=LCD_RD_DATA(); //这里读回 0X10
if(tmp_id == 0x5310)
lcddev.id = 0x5310;
}
if(lcddev.id ==0XFFFF) //也不是 NT35310,尝试看看是不是 NT35510
{
LCD_WR_REG(0XDA00);
tmp_id=LCD_RD_DATA(); //读回 0X00
LCD_WR_REG(0XDB00);
tmp_id=LCD_RD_DATA(); //读回 0X80
tmp_id<<=8;
LCD_WR_REG(0XDC00);
tmp_id|=LCD_RD_DATA(); //读回 0X00
if(tmp_id==0x8000)
lcddev.id=0x5510;
//NT35510 读回的 ID 是 8000H,为方便区分,我们强制设置为 5510
}
if(lcddev.id ==0XFFFF) //也不是 NT5510,尝试看看是不是 SSD1963
{
LCD_WR_REG(0XA1);
tmp_id=LCD_RD_DATA();
tmp_id=LCD_RD_DATA(); //读回 0X57
tmp_id<<=8;
tmp_id |=LCD_RD_DATA(); //读回 0X61
if(tmp_id==0X5761)
lcddev.id=0X1963;
//SSD1963 读回的 ID 是 5761H,为方便区分,我们强制设置为 1963
}
printf(" LCD ID:%x\r\n",lcddev.id); //打印 LCD ID
if(lcddev.id==0X9341) //9341 初始化
{
……//9341 初始化寄存器序列
}
else if(lcddev.id==0xXXXX) //其他 LCD 初始化代码
{
……//其他 LCD 驱动 IC,初始化代码
}
LCD_Display_Dir(0); //默认为竖屏显示
LCD_LED=1; //点亮背光
LCD_Clear(WHITE);
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
在main函数中,使用了一个FSM来控制操作步。
u8 x=0;
u8 lcd_id[12]; //存放 LCD ID 字符串
...
LCD_Init(); //初始化 LCD FSMC 接口
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,"2017/4/8");
x++;
if(x==12)
x=0;
LED0=!LED0;
delay_ms(1000);
}
sprintf 的函数,该函数用法同 printf,只是 sprintf把打印内容输出到指定的字符串数组中。
+++++++++++++++++++++++++++++++++++++++
实例,
在cubemx中配置FSMC支持LCD。
选择SRAM1,
设置CS为NE1,
设置LCD REG SEL 为A6,
设置DATA为16bits,
在SRAM1 timing中 ,
设置access mode 为A,
设置ADDSET需要的HCLK数,为15,
设置DATAST需要的HCLK数,为60,
设置Bus turn around需要的HCLK数,为0,
这是控制的读时序,
再设置写时序,
设置extend access mode 为A,
设置extend ADDSET需要的HCLK数,为9,
设置extend DATAST需要的HCLK数,为9,
设置extend Bus turn around需要的HCLK数,为0,