框架
将程序模块化,一个功能由一个.c文件完成,并且将这些功能分层。
- (上)主程序main.c、draw.c:完成主程序总流程,“初始化->关键变量设置->LCD显示操作”。
- (中)Display_manager部分、Fonts_manager部分、Encoding_manager部分、input_manager部分、debug_manager部分:各自管理维护一条链表,提供上层匹配以及初始化接口,提供下层注册。
- (底)各子功能的定义及初始化,注册到对应manager维护的链表中。
关键代码思路
一. Encoding部分:
文本的编码格式有:ANSI、unicode、unicode big endian、UTF-8 共四种。
- ANSI:英文用ASCII码表示(一字节),汉字用GBK国标码表示(两字节)。
问题:如何通过码值知道该字符是英文还是汉字呢?
可通过码值大小来确定,英文码值<0x80,汉字码值>=0x80。 - unicode:小端存储方式,两字节表示,文件头部用0xFF, 0xFE两字节来表示。
- unicode big endian:大端存储方式,两字节表示,文件头部用0xFE, 0xFF两字节来表示。
- UTF-8:小端存储方式,可变长字节表示,文件头部用0xEF, 0xBB, 0xBF三字节来表示。
从上可知,当我们打开一个未知编码格式的文本文件(电子书)时,可以根据头部信息来判断其编码格式,从而可以根据其编码格式,采取对应的解码方式及获取点阵方式。
解码方式:
- ANSI: 通过头部信息判断为ANSI编码格式,那么解码时读一个字节若码值小于0x80则直接通过该码值获取对应点阵,若大于0x80则继续读入下一个字节(两字节)。
对应代码:
static int AsciiGetCodeFrmBuf(unsigned char *pucBufStart, unsigned char *pucBufEnd, unsigned int *pdwCode)
{
unsigned char *pucBuf = pucBufStart;
unsigned char c = *pucBuf;
if ((pucBuf < pucBufEnd) && (c < (unsigned char)0x80))
{
/* 返回ASCII码 */
*pdwCode = (unsigned int)c;
return 1;
}
if (((pucBuf + 1) < pucBufEnd) && (c >= (unsigned char)0x80))
{
/* 返回GBK码 */
*pdwCode = pucBuf[0] + (((unsigned int)pucBuf[1])<<8);
return 2;
}
if (pucBuf < pucBufEnd)
{
/* 可能文件有损坏, 但是还是返回一个码, 即使它是错误的 */
*pdwCode = (unsigned int)c;
return 1;
}
else
{
/* 文件处理完毕 */
return 0;
}
}
- unicode:
static int Utf16leGetCodeFrmBuf(unsigned char *pucBufStart, unsigned char *pucBufEnd, unsigned int *pdwCode)
{
if (pucBufStart + 1 < pucBufEnd)
{
*pdwCode = (((unsigned int)pucBufStart[1])<<8) + pucBufStart[0];
return 2;
}
else
{
/* 文件结束 */
return 0;
}
}
- unicode big endian:
static int Utf16beGetCodeFrmBuf(unsigned char *pucBufStart, unsigned char *pucBufEnd, unsigned int *pdwCode)
{
if (pucBufStart + 1 < pucBufEnd)
{
*pdwCode = (((unsigned int)pucBufStart[0])<<8) + pucBufStart[1];
return 2;
}
else
{
/* 文件结束 */
return 0;
}
}
- UTF-8:较复杂
对于UTF-8编码中的任意字节B,如果B的第一位为0,则B为ASCII码,并且B独立的表示一个字符;
如果B的第一位为1,第二位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的一个字节,并且不为字符的第一个字节编码;
如果B的前两位为1,第三位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由两个字节表示;
如果B的前三位为1,第四位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由三个字节表示;
如果B的前四位为1,第五位为0,则B为一个非ASCII字符(该字符由多个字节表示)中的第一个字节,并且该字符由四个字节表示;
因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;
根据前二位,可判断该字节是否为一个字符编码的第一个字节;
根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;
根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。
static int Utf8GetCodeFrmBuf(unsigned char *pucBufStart, unsigned char *pucBufEnd, unsigned int *pdwCode)
{
int i;
int iNum;
unsigned char ucVal;
unsigned int dwSum = 0;
if (pucBufStart >= pucBufEnd)
{
/* 文件结束 */
return 0;
}
ucVal = pucBufStart[0];
iNum = GetPreOneBits(pucBufStart[0]);
if ((pucBufStart + iNum) > pucBufEnd)
{
/* 文件结束 */
return 0;
}
if (iNum == 0)
{
/* ASCII */
*pdwCode = pucBufStart[0];
return 1;
}
else
{
ucVal = ucVal << iNum;
ucVal = ucVal >> iNum;
dwSum += ucVal;
for (i = 1; i < iNum; i++)
{
ucVal = pucBufStart[i] & 0x3f;
dwSum = dwSum << 6;
dwSum += ucVal;
}
*pdwCode = dwSum;
return iNum;
}
}
获取点阵方式:
- ascii码: (unsigned char *)&fontdata_8x16[码值 * 16];
- HZK16:汉字库映射起始位置 + (高字节* 94 + 低字节)*32;
- freetype:g_tSlot->bitmap.buffer; // 通过FT_Load_Char将glyph关键点信息转换为位图
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二. Fonts部分:
字体在显示时,需要确定显示的坐标和字体大小等参数,ANSI与GBK的每个字体点阵之间的距离固定,ascii为8 * 16,HZK16为16 * 16,而freetype的每个字体点阵大小不固定,间距是变化的,因此要构造一个通用结构体来表示会比较复杂。
问题1:为什么要构造一个通用结构体来描述他们呢?
答: 一个通用的结构体兼容具有差异性的功能,方便管理和使用,简化框架,可实现统一且通用的接口函数。
问题2:如何构造?
三种点阵存储方式如下图:
分析:A每一行的跨度为1字节,B的每一行的跨度为2字节,C的每一行跨度slot->bitmao->pich获取(结果向上取整,例如17位则用3字节表示),对于A\B来说十分简单,但是为兼容freetype的显示需引入其他成员。
关于freetype的显示:
第一需要进行LCD坐标转换;
第二字体点阵的原点坐标为(pen.x + advance.x, pen.y + advance.y),但是注意他所显示的坐标不一定为原点坐标,会有偏差;
第三,点阵的表示有两种情况,MONO位图:1bit表示一个像素;anti-color:1Byte表示一个像素。
知道了上述知识点,我们可以定义如下通用结构体:
typedef struct FontBitMap {
int iXLeft;
int iYTop; //该字体显示的LCD坐标
int iXMax;
int iYMax; //该字体最大的长度
int iBpp; //freetype像素的表示方式
int iPitch; /* 对于单色位图, 两行象素之间的跨度 */
int iCurOriginX;
int iCurOriginY; //当前原点坐标
int iNextOriginX;
int iNextOriginY; //下一个原点坐标
unsigned char *pucBuffer; //指向点阵的存储空间
}T_FontBitMap, *PT_FontBitMap;
问题三:如何设置freetype字体间距
实际LCD显示坐标原点计算:
- iPenX = iCurOriginX;
- iPenY = iCurOriginY;
- iXLeft = iPenX + slot->bitmap_left;
- iYTop = iPenY - slot->bitmap_top; //这里注意 y 轴方向相反了,用 - 号
- iXMax = iXLeft + slot->bitmap->width;
- iYMax = iYTop + slot->bitmap->rows;
- iNextOriginX = iPenX + slot->advance.x / 64;
- iNextOriginY = iPenY;
问题四:由于freetype字体大小无法确定,如何保证显示字体使不会溢出屏幕?
1点:将字体显示的当前x坐标与字体横向长度之和 iXMax,与屏幕的xres分辨率作比较,如果溢出则需换行。
2点:将字体显示的当前y坐标与字体纵向长度之和 iYMax,与屏幕的yres分辨率作比较,如果溢出则需换页。
iXMax > var.xres 则需换行
iYMax > var.yres 则需换页
换行操作代码:
iDeltaX = 0 - iCurOriginX;
iDeltaY = FontSize;
iCurOriginX += iDeltaX;
iCurOriginY += iDeltaY;
iNextOriginX += iDeltaX;
iNextOriginY += iDeltaY;
iXLeft += iDeltaX;
iXMax += iDeltaX;
iYTop += iDeltaY;
iYMax += iDeltaY;;
换页操作:
- 清屏
- iCurOriginX = 0;
iCurOriginY = FontSize;
pucTextFileMemCurPos = 显示了多少字节 + 起始地址;
pucBufStart = pucTextFileMemCurPos;
三.freetype的字体颜色显示方式
MONO位图:1bit表示一个像素;//单色图,底色为黑
anti-color:1Byte表示一个像素; //彩色(反色)位图,底色可以设置
区别:
单色说直白一点就是一种颜色,比如红,黄,蓝绿,而彩色的就有多重颜色混搭,这就更加具有视觉感官。
数据大小不同,用彩色位图表示的一个字体数据量为单色图的八倍。
int ShowOneFont(PT_FontBitMap ptFontBitMap)
{
int x;
int y;
unsigned char ucByte = 0;
int i = 0;
int bit;
if (ptFontBitMap->iBpp == 1)
{
for (y = ptFontBitMap->iYTop; y < ptFontBitMap->iYMax; y++)
{
i = (y - ptFontBitMap->iYTop) * ptFontBitMap->iPitch;
for (x = ptFontBitMap->iXLeft, bit = 7; x < ptFontBitMap->iXMax; x++)
{
if (bit == 7)
{
ucByte = ptFontBitMap->pucBuffer[i++];
}
if (ucByte & (1<<bit))
{
g_ptDispOpr->ShowPixel(x, y, COLOR_FOREGROUND);
}
else
{
/* 使用背景色, 不用描画 */
// g_ptDispOpr->ShowPixel(x, y, 0); /* 黑 */
}
bit--;
if (bit == -1)
{
bit = 7;
}
}
}
}
else if (ptFontBitMap->iBpp == 8)
{
for (y = ptFontBitMap->iYTop; y < ptFontBitMap->iYMax; y++)
for (x = ptFontBitMap->iXLeft; x < ptFontBitMap->iXMax; x++)
{
//g_ptDispOpr->ShowPixel(x, y, ptFontBitMap->pucBuffer[i++]);
if (ptFontBitMap->pucBuffer[i++])
g_ptDispOpr->ShowPixel(x, y, COLOR_FOREGROUND);
}
}
else
{
DBG_PRINTF("ShowOneFont error, can't support %d bpp\n", ptFontBitMap->iBpp);
return -1;
}
return 0;
}
三. 文本的读取
通过指定的解码方式,按一定字节循环读取出,特殊字符:换行,制表符等。
- 普通字符:根据点阵显示对应字体。
- 文本换行有两种"\n\r",“\n”: 只处理“\n”,"\r"不做处理
- 制表符“\t”:打印空格
- 读取0字节:文本已读取完毕
四. 如何显示一页
int ShowOnePage(unsigned char *pucTextFileMemCurPos)
{
int iLen;
int iError;
unsigned char *pucBufStart;
unsigned int dwCode;
PT_FontOpr ptFontOpr;
T_FontBitMap tFontBitMap;
int bHasNotClrSceen = 1;
int bHasGetCode = 0;
tFontBitMap.iCurOriginX = 0;
tFontBitMap.iCurOriginY = g_dwFontSize;
pucBufStart = pucTextFileMemCurPos;
while (1)
{
iLen = g_ptEncodingOprForFile->GetCodeFrmBuf(pucBufStart, g_pucTextFileMemEnd, &dwCode);
if (0 == iLen)
{
/* 文件结束 */
if (!bHasGetCode)
{
return -1;
}
else
{
return 0;
}
}
bHasGetCode = 1;
pucBufStart += iLen;
/* 有些文本, \n\r两个一起才表示回车换行
* 碰到这种连续的\n\r, 只处理一次
*/
if (dwCode == '\n')
{
g_pucLcdNextPosAtFile = pucBufStart;
/* 回车换行 */
tFontBitMap.iCurOriginX = 0;
tFontBitMap.iCurOriginY = IncLcdY(tFontBitMap.iCurOriginY);
if (0 == tFontBitMap.iCurOriginY)
{
/* 显示完当前一屏了 */
return 0;
}
else
{
continue;
}
}
else if (dwCode == '\r')
{
continue;
}
else if (dwCode == '\t')
{
/* TAB键用一个空格代替 */
dwCode = ' ';
}
DBG_PRINTF("dwCode = 0x%x\n", dwCode);
ptFontOpr = g_ptEncodingOprForFile->ptFontOprSupportedHead;
while (ptFontOpr)
{
DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
iError = ptFontOpr->GetFontBitmap(dwCode, &tFontBitMap);
DBG_PRINTF("%s %s %d, ptFontOpr->name = %s, %d\n", __FILE__, __FUNCTION__, __LINE__, ptFontOpr->name, iError);
if (0 == iError)
{
DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
if (RelocateFontPos(&tFontBitMap))
{
/* 剩下的LCD空间不能满足显示这个字符 */
return 0;
}
DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
if (bHasNotClrSceen)
{
/* 首先清屏 */
g_ptDispOpr->CleanScreen(COLOR_BACKGROUND);
bHasNotClrSceen = 0;
}
DBG_PRINTF("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 显示一个字符 */
if (ShowOneFont(&tFontBitMap))
{
return -1;
}
tFontBitMap.iCurOriginX = tFontBitMap.iNextOriginX;
tFontBitMap.iCurOriginY = tFontBitMap.iNextOriginY;
g_pucLcdNextPosAtFile = pucBufStart;
/* 继续取出下一个编码来显示 */
break;
}
ptFontOpr = ptFontOpr->ptNext;
}
}
return 0;
}
五. 上一页与下一页的实现
通过双向链表实现上一页和下一页功能。
双向链表的结构:
typedef struct PageDesc {
int iPage; //当前第几页
unsigned char *pucLcdFirstPosAtFile; //当前页对应电子书的内容位置
unsigned char *pucLcdNextPageFirstPosAtFile; //下一页对应电子书的内容位置
struct PageDesc *ptPrePage;
struct PageDesc *ptNextPage;
} T_PageDesc, *PT_PageDesc;
将当前页的上一页与下一页对应的文件起始位置记录下来,使用尾插法添加到该双向链表中。(这里较难理解)
将每一页的页码即对应电子书的内容位置记录下来构建成结点,通过双向链表连接起来。
六. input_manager部分
支持标准输入及触摸屏。
问题1:通过fgetc从标准输入获取字节,输入完毕需按下Enter将内核缓冲区读出到用户空间,如何实现一按下无需按Enter键则立即读出?
关闭终端canonical模式,并且设置c_cc[VMIN]为1,表示缓冲区有一个字节则立即读出到用户空间缓冲区
struct termios tTTYState;
//get the terminal state
tcgetattr(STDIN_FILENO, &tTTYState);
//turn off canonical mode
tTTYState.c_lflag &= ~ICANON;
//minimum of number input read.
tTTYState.c_cc[VMIN] = 1; /* 有一个数据时就立刻返回 */
//set the terminal attributes.
tcsetattr(STDIN_FILENO, TCSANOW, &tTTYState);
反操作:
struct termios tTTYState;
//get the terminal state
tcgetattr(STDIN_FILENO, &tTTYState);
//turn on canonical mode
tTTYState.c_lflag |= ICANON;
//set the terminal attributes.
tcsetattr(STDIN_FILENO, TCSANOW, &tTTYState);
问题2:输入系统IO模型如何选择:
非阻塞:采用轮询不断读取,大量占用CPU
多线程:主线程阻塞,通过子线程唤醒。
多路复用:主线程阻塞,通过内核唤醒。
若支持非阻塞标准输入的设置: 可利用select的超时设置来实现
struct timeval tTV;
fd_set tFDs;
tTV.tv_sec = 0;
tTV.tv_usec = 0;
FD_ZERO(&tFDs);
FD_SET(STDIN_FILENO, &tFDs);
select(STDIN_FILENO + 1, &tFDs, NULL, NULL, &tv);
if(FD_ISSET(STDIN_FILENO, &tFDs))
{
..........
}
若支持触摸屏非阻塞访问,会导致按一下产生会多次事件,通过定义按键事件的timeval值,判断当前按下触摸屏距离上次事件发生超过0.5秒则再次触发input事件。
static int isOutOf500ms(struct timeval *ptPreTime, struct timeval *ptNowTime)
{
int iPreMs;
int iNowMs;
iPreMs = ptPreTime->tv_sec * 1000 + ptPreTime->tv_usec / 1000;
iNowMs = ptNowTime->tv_sec * 1000 + ptNowTime->tv_usec / 1000;
return (iNowMs > iPreMs + 500);
}
若支持多线程访问,通过条件变量与互斥锁实现同步操作:
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_tConVar = PTHREAD_COND_INITIALIZER;
static void *InputEventTreadFunction(void *pVoid)
{
T_InputEvent tInputEvent;
/* 定义函数指针 */
int (*GetInputEvent)(PT_InputEvent ptInputEvent);
GetInputEvent = (int (*)(PT_InputEvent))pVoid;
while (1)
{
if(0 == GetInputEvent(&tInputEvent))
{
/* 唤醒主线程, 把tInputEvent的值赋给一个全局变量 */
/* 访问临界资源前,先获得互斥量 */
pthread_mutex_lock(&g_tMutex);
g_tInputEvent = tInputEvent;
/* 唤醒主线程 */
pthread_cond_signal(&g_tConVar);
/* 释放互斥量 */
pthread_mutex_unlock(&g_tMutex);
}
}
return NULL;
}
int AllInputDevicesInit(void)
{
PT_InputOpr ptTmp = g_ptInputOprHead;
int iError = -1;
while (ptTmp)
{
if (0 == ptTmp->DeviceInit())
{
/* 创建子线程 */
pthread_create(&ptTmp->tTreadID, NULL, InputEventTreadFunction, ptTmp->GetInputEvent);
iError = 0;
}
ptTmp = ptTmp->ptNext;
}
return iError;
}
int GetInputEvent(PT_InputEvent ptInputEvent)
{
/* 休眠 */
pthread_mutex_lock(&g_tMutex);
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 被唤醒后,返回数据 */
*ptInputEvent = g_tInputEvent;
pthread_mutex_unlock(&g_tMutex);
return 0;
}
七. debug_manager部分
支持串口打印、网络远程打印:
- 打印协议:
#define APP_EMERG "<0>" /* system is unusable */
#define APP_ALERT "<1>" /* action must be taken immediately */
#define APP_CRIT "<2>" /* critical conditions */
#define APP_ERR "<3>" /* error conditions */
#define APP_WARNING "<4>" /* warning conditions */
#define APP_NOTICE "<5>" /* normal but significant condition */
#define APP_INFO "<6>" /* informational */
#define APP_DEBUG "<7>" /* debug-level messages */
/*注意:打印级别 0 最高 */
#define DEFAULT_DBGLEVEL 4 //默认打印级别
typedef struct DebugOpr {
char *name; //名字
int isCanUse; //是否打开
int (*DebugInit)(void); //初始化
int (*DebugExit)(void); //释放,反操作
int (*DebugPrint)(char *strData); //打印方法
struct DebugOpr *ptNext; //链表
}T_DebugOpr, *PT_DebugOpr;
- 关于可变参及级别限制的打印功能统一接口函数实现:
int DebugPrint(const char *pcFormat, ...)
{
char strTmpBuf[1000];
char *pcTmp;
va_list tArg;
int iNum;
PT_DebugOpr ptTmp = g_ptDebugOprHead;
int dbglevel = DEFAULT_DBGLEVEL;
va_start (tArg, pcFormat);
iNum = vsprintf (strTmpBuf, pcFormat, tArg);
va_end (tArg);
strTmpBuf[iNum] = '\0';
pcTmp = strTmpBuf;
/* 根据打印级别决定是否打印 */
if ((strTmpBuf[0] == '<') && (strTmpBuf[2] == '>'))
{
dbglevel = strTmpBuf[1] - '0';
if (dbglevel >= 0 && dbglevel <= 9)
{
pcTmp = strTmpBuf + 3;
}
else
{
dbglevel = DEFAULT_DBGLEVEL;
}
}
if (dbglevel > g_iDbgLevelLimit)
{
return -1;
}
/* 调用链表中所有isCanUse为1的结构体的DebugPrint函数 */
while (ptTmp)
{
if (ptTmp->isCanUse)
{
ptTmp->DebugPrint(pcTmp);
}
ptTmp = ptTmp->ptNext;
}
return 0;
}
ptTmp->DebugPrint(pcTmp)调用各注册进来的对象DebugPrint方法:
对于串口:直接使用printf函数输出到终端。
对于网络打印:定义个环形缓冲区(通过(Pos + 1) % BUF_SIZE实现),将数据存放在该缓冲区中,文本发送数据量大推荐使用UDP发送,缓冲区大小尽量要大于一次数据包发送量,每次发送均从环形缓冲区取适当的数据量到临时缓冲区进行发送,保证传输效率高且丢包率低,通过sendto发送给客户端。
网络打印代码实现如下:
static int NetDbgPrint(char *strData)
{
/* 把数据放入环形缓冲区 */
int i;
for (i = 0; i < strlen(strData); i++)
{
if (0 != PutData(strData[i]))
break;
}
//唤醒休眠
return i;
}
static void *NetDbgSendTreadFunction(void *pVoid)
{
char strTmpBuf[512];
char cVal;
int i;
int iAddrLen;
int iSendLen;
while (1)
{
/* 平时休眠 */
while (g_iHaveConnected && !isEmpty())
{
i = 0;
/* 把环形缓冲区的数据取出来, 最多取512字节 */
while ((i < 512) && (0 == GetData(&cVal)))
{
strTmpBuf[i] = cVal;
i++;
}
/* 执行到这里, 表示被唤醒 */
/* 用sendto函数发送打印信息给客户端 */
iAddrLen = sizeof(struct sockaddr);
iSendLen = sendto(g_iSocketServer, strTmpBuf, i, 0,
(const struct sockaddr *)&g_tSocketClientAddr, iAddrLen);
}
}
return NULL;
}
总体思路
上层首先通过manager层初始化已注册链表上的设备,并将链表进行维护,即包含该框架底层支持的功能。
通过传入的电子书文件,判断其编码格式,从而通过Encoding中所维护的链表获取匹配的解码方式。
通过传入的字体类型来判断要使用的Fonts字体类型,从Fonts链表中获取对应的字体相关信息。
将所有对应信息赋值给draw.c中的关键变量,根据变量信息获取字体点阵、解码方式,根据传入的指令显示页面或退出。