在TFTLCD屏上显示中英文文本文件是本次硬件课程设计的基本要求,也是我设计的多功能播放器最重要的功能,要求能够读取事先存储在SD卡中的文本文件,解析后既能显示中英文字符,又能避免中文乱码。同时模拟小说阅读器对文本文件实现翻页功能,用按键1和按键2进行页面切换,按键3返回到目录。
•SPI简介
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是 Motorola 公司推出的一种同步串行接口技术,是一种高速的,全双工,同步的通信总线。具有通信简单、数据传输速率快以及穿双工通信等优点。但由于没有指定的流控制以及没有应答机制确认是否收到数据而在数据可靠性上存在一定缺陷。
SPI由于接口相对简单,用途算是比较广泛,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。即一个SPI的Master通过SPI与一个从设备,即上述的那些Flash,ADC等,进行通讯。在保证主从设备两者之间时钟SCLK一致的条件下,即保证主从设备时序上的一致,即可完成主从设备之间的SPI正常通讯。
•SPI接口
SPI常作为单片机外设芯片串行扩展接口,以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,一般需要4根接口线(单向传输时3根线即可)。所有基于SPI的设备都包含4个引脚:MISO(数据输入)、MOSI(数据输出)、SCLK(时钟)、CS(片选)。
1)MOSI:主设备数据输出,从设备数据输入;
2)MISO:主设备数据输入,从设备数据输出;
3)SCLK:时钟信号,由主设备产生;
4)CS/SS:从设备使能信号,由主设备控制。当有多个从设备的时候,因为每个从设备上都有一个片选引脚接入到主设备机中,当我们的主设备和某个从设备通信时将需 要将从设备对应的片选引脚电平拉低或者是拉高。
•SPI通信模式
SPI通信有4种不同的模式,不同的从设备可能在出厂是就是配置为某种模式,这是不能改变的;但我们的通信双方必须是工作在同一模式下,所以我们可以对我们的主设备的SPI模式进行配置,通过CPOL(Clock Polarity 时钟极性)和CPHA(Clock Phase 时钟相位)来控制我们主设备的通信模式,具体如下:
Mode0:CPOL=0,CPHA=0;Mode1:CPOL=0,CPHA=1
Mode2:CPOL=1,CPHA=0;Mode3:CPOL=1,CPHA=1
时钟极性CPOL是用来配置SCLK的电平出于哪种状态时是空闲态或者有效态,时钟相位CPHA是用来配置数据采样是在第几个边沿:
CPOL=0,表示当SCLK=0时处于空闲态,所以有效状态就是SCLK处于高电平时
CPOL=1,表示当SCLK=1时处于空闲态,所以有效状态就是SCLK处于低电平时
CPHA=0,表示数据采样是在第1个边沿,数据发送在第2个边沿
CPHA=1,表示数据采样是在第2个边沿,数据发送在第1个边沿
•配置SPI
void SPI1_Init(void)
{
//SPI和GPIO结构体
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
//SPI和GPIO时钟使能
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1, ENABLE );
//配置SCLK、MOSI和MISO三个GPIO引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//拉高片选线
GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);
//设置数据模式
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置工作模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置数据大小
//设置通信模式
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS由软件管理
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//分频值
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据高位先传
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算多项式
SPI_Init(SPI1, &SPI_InitStructure);
//使能SPI
SPI_Cmd(SPI1, ENABLE);
}
SPI配置步骤如下:
1)使能SPI和GPIO时钟
2)配置SCLK、MOSI和MISO三个GPIO引脚
3)拉高片选线
4)配置SPI控制寄存器
5)使能SPI
•W25Q128简介
1)W25Q128 是华邦公司推出的一款 SPI 接口的 NOR Flash 芯片,其存储空间为 128Mbit,相当于 16M 字节。W25Q128 可以支持 SPI 的模式 0 和模式 3,也就是 CPOL=0/CPHA=0 和CPOL=1/CPHA=1 这两种模式。
2)写入数据时,需要注意以下两个重要问题:
①Flash 写入数据时和 EEPROM 类似,不能跨页写入,一次最多写入一页,W25Q128的一页是 256 字节。写入数据一旦跨页,必须在写满上一页的时候,等待 Flash 将数据从缓存搬移到非易失区,重新再次往里写。
②Flash 有一个特点,就是可以将 1 写成 0,但是不能将 0 写成 1,要想将 0 写成 1,必须进行擦除操作。因此通常要改写某部分空间的数据,必须首先进行一定物理存储空间擦除,最小的擦除空间,通常称之为扇区,扇区擦除就是将这整个扇区每个字节全部变成 0xFF。每款 Flash 的扇区大小不一定相同,W25Q128 的一个扇区是 4096 字节。为了提高擦除效率,使用不同的擦除指令还可以一次性进行 32K(8 个扇区)、64K(16 个扇区)以及整片擦除。
3)W25Q128 内部有一个“SPI Command & Control Logic”,可以通过 SPI 接口向其发送指令,从而执行相应操作。指令的长度是不定的,有单字节的,也有多字节的,W25Qxx 一共具有 34 个操作指令,在此只列举常用的 12 个。
•W25Q128主要驱动函数
函数原型:void W25qx_Init(void);
描述:W25Q128初始化函数,完成SPI的配置,检查中文字库,以及读取芯片型号以确定通讯正常。
函数原型:void W25qxx_ReadBuffer ( u32 addr, u8 *p, u16 num);
描述:从W25Q128中读取数据到指定内存区域。
参数:
u32 addr 读取地址
u8 *p 读出的数值存放位置
u16 num 连续读取的字节数
函数原型:void W25qxx_WriteBuffer( u32 addr, u8 *p, u16 num);
描述:向W25Q128中写数据,写之前查出该区域数据,同时防止向字库存储区域写数据
参数:
u32 addr 写入地址
u8 *p 要写入的数据存储器
u16 num 连续写入的字节数
•SD卡简介
SD存储卡(Secure Digital Memory Card)是一种基于半导体快闪存储器的新一代高速存储设备。SD存储卡的技术是从MMC卡(MultiMedia Card格式上发展而来,在兼容SD存储卡基础上发展了SDIO(SD Input/ Output)卡,此兼容性包括机械,电子,电力,信号和软件,通常将SD、SDIO卡俗称SD存储卡。
一张SD卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器5 个部分。存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证SD卡工作在合适的电压下,如出现掉电或上状态时,它会使控制单元和存储单元接口复位;卡及接口控制单元控制SD卡的运行状态,它包括有8个寄存器。
SD卡有两种驱动模式:SPI模式与SDIO模式。它们所使用的接口信号是不同的。在SPI模式下,只会用到SD卡的4根信号线,即CS、DI、SCLK与DO(分别是SD卡的片选、数据输入、时钟与数据输出)。
SD卡共支持三种传输模式:SPI模式(独立序列输入和序列输出),1位SD模式(独立指令和数据通道,独有的传输格式),4位SD模式(使用额外的针脚以及某些重新设置的针脚。支持四位宽的并行传输)。
•SD卡主要驱动函数
函数原型:u8 SD_Initialize(void)
描述:初始化SD卡
函数原型:u8 SD_ReadDisk(u8 *buf,u32 sector,u8 cnt)
描述:读SD卡
参数:
U8 *buf 读出的数据在内存中的缓存区
U32 sector 开始读的扇区位置
U8 cnt 连续读取的扇区数
函数原型:u8 SD_WriteDisk(u8 *buf,u32 sector,u8 cnt)
描述:写SD卡
参数:
u8 *buf 写入数据在内存中的缓存区
u32 sector 开始写入的扇区位置
u8 cnt 连续写入的扇区数
•TFTLCD显示屏简介
TFT-LCD(thin film transistor-liquid crystal display)即薄膜晶体管液晶显示器。液晶显示屏的每一个像素上都设置有一个薄膜晶体管(TFT),每个像素都可以通过点脉冲直接控制,因而每个节点都相对独立,并可以连续控制,不仅提高了显示屏的反应速度,同时可以精确控制显示色阶,所以TFT液晶的色彩更真,因此TFT-LCD也被叫做真彩液晶显示器。
常用的TFT液晶屏接口有8位、9位、16位、18位,这里的位数表示的是彩屏数据线的数量。常用的通信模式有6800模式和8080模式,其中8080模式的接口有5条基本的控制线和多条数据线(8/9/16/18位),它们的功能如下表:
本此实验采用的是1.8寸TFT LCD液晶屏,驱动芯片为ST7735,SPI接口的引脚有8脚,其中GND和VCC是液晶的电源引脚,VCC接3.3V。SCL和SDA分别为SPI的时钟信号线和数据线。RES为LCD的复位信号,可以有STM32控制其复位。DC为数据/命令选择端,低电平写命令,高电平写数据。CS为液晶屏片选信号,低电平使能;BL为背光信号,低电平关闭背光。
•TFTLCD主要驱动函数
函数原型:void LCD_Init(void)
描述:初始化LCD
函数原型:void LCD_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 color)
描述:在指定区域内填充单个颜色
参数:
u16 sx, u16 sy 左上角起始坐标
u16 ex, u16 ey 右下角结束坐标
u16 color 填充的颜色
函数原型:void drawAscii(u16 x,u16 y,u8 num,u8 size,u32 fColor, u32 bColor)
描述:在指定位置显示一个字符
参数:
u16 x, u16 y 起始坐标
u8 num 要显示的字符,从‘ ’到‘~’
u8 size 字体大小 12/16/24/32
u32 fColor 字体颜色
u32 bColor 背景颜色
函数原型:void LCD_String(u16 x, u16 y, char* pFont, u8 size, u32 fColor, u32 bColor)
描述:在屏幕上显示字符串,支持中英文
参数:
u16 x, u16 y 起始坐标
char *pFont 字符串储存位置
u8 size 字体大小
u32 fColor 字体颜色
u32 bColor 背景颜色
函数原型:void LCD_Num(u16 x, u16 y, u16 num, u8 size, u32 fColor, u32 bColor)
描述:在屏幕上显示数值
参数:
u16 x, u16 y 起始坐标
u16 num 要显示的数值大小,0~999
u8 size 字体大小
u32 fColor 字体颜色
u32 bColor 背景颜色
函数原型:void LCD_Image(u16 x, u16 y, u16 width, u16 height, const u8 *image)
描述:在指定区域填充指定图片数据
参数:
u16 x, u16 y 起始坐标
u16 width 图片宽度
u16 height 图片高度
const u8 *image 数据缓存地址
•FATFS简介
适合嵌入式小型单片机,是一个独立的软件层文件系统,我们只需要将底层硬件的读取函数移植到FATFS提供的向下的接口(Media Access Interface),完成之后,就可以像电脑一样使用文件的操作函数(FATFS提供的向上的供我们使用的API函数)。
FATFS模块的层次结构如下图示:
最顶层是应用层:使用者只需要调用FATFS模块提供给用户的一系列应用接口函数(如f_open, f_read, f_write和f_close等),就可以像在PC上读写文件那样简单。
中间层FATFS模块:实现了FAT文件读写协议;它提供了ff.c和ff.h文件,一般情况下不用修改,使用时将头文件包含进去即可。
最底层是FATFS模块的底层接口:包括存储媒介读写接口和供给文件创建修改时间的实时时钟,需要在移植时编写对应的代码。
•移植FATFS步骤
1)修改diskio.c文件(与硬件相关的底层驱动)
2)修改ffconf.c文件(修改相关的宏)
3)格式化文件系统(如果已有文件系统可以不用格式化)
•汉字显示原理
汉字在液晶上的显示其实就是一些点的显示与不显示,这就相当于我们的笔一样,有笔经过的地方就画出来,没经过的地方就不画。所以要显示汉字,我们首先要知道汉字的点阵数据,这些数据可以由专门的软件来生成。只要知道了一个汉字点阵的生成方法,那么我们在程序里面就可以把这个点阵数据解析成一个汉字。知道显示了一个汉字,就可以推及整个汉字库了。
•中文编码
常用的汉字内码系统有 GB2312,GB13000,GBK,BIG5(繁体)等几种,其中 GB2312支持的汉字仅有几千个,很多时候不够用,而 GBK 内码不仅完全兼容 GB2312,还支持了繁体字,总汉字数有 2 万多个,完全能满足我们一般应用的要求。
•字库查找
汉字在各种文件里面是以内码的形式存储的,每个汉字对应着一个内码,在知道了内码之后再去字库里面查找这个汉字的点阵数据,然后在液晶上显示出来。我们要解决的最大问题就是制作一个与汉字内码对得上号的汉字点阵库,而且要方便单片机的查找。
每个 GBK 码由 2 个字节组成,第一个字节为 0X81~0XFE,第二个字节分为两部分,一是 0X40~0X7E,二是 0X80~0XFE。我们把第一个字节代表的意义称为区,那么 GBK 里面总共有 126 个区(0XFE-0X81+1),每个区内有 190 个汉字(0XFE-0X80+0X7E-0X40+2),总共就有 126*190=23940 个汉字。我们的点阵库只要按照这个编码规则从 0X8140 开始,逐一建立,每个区的点阵大小为每个汉字所用的字节数*190。这样,我们就可以得到在这个字库里面定位汉字的方法:
GBKL<0X7F :Hp=((GBKH-0x81)*190+GBKL-0X40)*csize;
GBKL>0X80 :Hp=((GBKH-0x81)*190+GBKL-0X41)*csize;
其中 GBKH、GBKL 分别代表 GBK 的第一个字节和第二个字节(也就是高位和低位),Hp为对应汉字点阵数据在字库里面的起始地址(假设是从 0 开始存放),csize 代表一个汉字点阵所占的字节数。
这样我们只要得到了汉字的 GBK 码,就可以得到该汉字点阵在点阵库里面的位置,从而获取其点阵数据,显示这个汉字了。
万事开头难,这部分工作是我在整个工程中花费时间最多,也是感觉最难的一部分工作。初次接触单片机,一切都感觉很陌生,好在有微机原理和操作系统的理论知识,对计算机系统有比较清晰的了解,并且很好地完成了微机实验中FPGA的设计工作,有了上述基础,再通过B站上正点原子的有关stm32教学视频的学习,我初步掌握了stm32系统架构,以及像系统时钟、中断、SPI、USART和定时器等基本操作,为下一步的学习打下坚实的基础。
初步学习了stm32后,我开始逐步移植W25Q128、SD卡和TFTLCD的驱动程序,移植后对主要的驱动函数进行测试,测试成功后再结合各模块的技术手册逐行读懂每一行代码,在读完整个驱动程序后,虽然对模块内部具体构造还不甚了了,但已经能熟练掌握模块的运用,并在此基础上开发出更多相关的功能。
不带文件系统的SD卡仅能实现简单的读写扇区操作,要真正应用SD卡,并在此基础上开发出多种多样的应用功能,就必须要使用文件系统。文件系统只要了解其基本的工作原理,移植起来就会比较顺利,配置后要对其进行多样测试,以检验文件系统是否移植成功。
•原理
在文本文件中,英文字符用一个字节存储,而中文字符是用两个字节存储,当读取文本文件的一定字节数时,读取的最后一个字节恰好是中文字符的第一个字节,就会造成后面的文件乱码。
英文字符编码是0~127,而中文字符第一个字节编码是128~255,所以可以通过顺序检查字节的编码从而区分中英文字符,检测到读取的最后一个字节是中文的第一个字节时,必须少读或者多读一个字节数,这样就解决了中英文乱码问题。
•程序源码
u8 Chinese2Char(u8 * buff, u8 size) //检测字符串最后一个字节是否是中文第一个字节,
{ //是返回0,不是返回1
u8 i = 0;
while(i != size - 1 && i != size -2) //顺序检测,直到最后剩下一个或两个字节
{
if(*buff < 128) {buff++;i++;}
else {buff += 2;i += 2;}
}
if(i == size - 2) //剩下最后两个字节
{
if(*buff > 128) return 0;
else
{
buff++;
if(*buff > 128) return 1;
else return 0;
}
}
if(i == size - 1) //剩下最后一个字节
{
if(*buff > 128) return 1;
else return 0;
}
return 0;
}
参数:
u8 *buff 要检测的字符串储存位置
u8 size 字符串大小
•程序流程
•程序代码
void reader() //小说阅读
{
vKey_Close(); //关闭按键中断
LCD_String(1, 40, "宰执天下", 32, YELLOW, BLACK); //显示小说信息一定时间
LCD_String(16, 85, "作者:cuslaa", 16, YELLOW, BLACK);
delay_ms(2000);
f_open(&file0, "1.txt", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);
rbuff[PAGE_SIZE] = '\0';
flag2 = 2;
vKey_Init(); //开中断
while(flag1 == 1) //按键3没有按下
{
delay_ms(200);
if(flag2 == 1) //按键1按下
{
if(ye > 1)
{
ye--;
count -= page[ye];
f_lseek(&file0, count - page[ye - 1]);
f_read(&file0, rbuff, page[ye - 1], &num_read);
if(page[ye - 1] == PAGE_SIZE - 1)
rbuff[PAGE_SIZE - 1] = '\0';
}
}
if(flag2 == 2) //按键2按下
{
if(f_size(&file0) - count > PAGE_SIZE)
{
f_read(&file0, rbuff, PAGE_SIZE, &num_read);
if(Chinese2Char((u8 *)rbuff, PAGE_SIZE)) //检测是否乱码
{
count += PAGE_SIZE - 1;
f_lseek(&file0, count);
rbuff[PAGE_SIZE - 1] = '\0';
page[ye] = PAGE_SIZE - 1;
} else
{
count += PAGE_SIZE;
page[ye] = PAGE_SIZE;
}
ye++;
} else if(f_size(&file0) - count > 0)
{
f_read(&file0, rbuff, PAGE_SIZE, &num_read);
page[ye] = f_size(&file0) - count;
count = f_size(&file0);
ye++;
}
}
if(flag2 != 0) //按键1、2都没有按下时就不重复显示,避免闪烁
{
LCD_Fill(1, 1, 129, 160, BLACK);
LCD_String(6, 10, rbuff, 16, YELLOW, BLACK);
sprintf(yebuff, "第%d页³", ye);
LCD_String(45, 145, yebuff, 12, YELLOW, BLACK);
}
flag2 = 0;
if(flag1 == 0) //按键3按下,关闭文件
f_close(&file0);
}
}
相关变量:
u8 flag1 按键3按下置0,没有按下置1
u8 falg2 按键1按下置1,按键2按下置2
#define PAGE_SIZE 宏定义,每一页固定字节数,方便修改
u8 page[100] 存放每一页字节数,乱码时比PAGE_SIZE少1个
char rbuff[PAGE_SIZE + 1] 读取的数据
u8 ye 页数
问题描述:LCD上显示英文字符较快,但显示中文字符较慢
我的思路:英文字库烧录到内部Flash中,读取速度快,中文字库存放在外部Flash中,通过SPI通信,读取速度慢,在将SPI三个从机的波特率设置为最大,即时钟的二分频后,中文字符的显示仍然较慢,暂时得不到更好的解决方法,除非更换主频更高的开发板。