之前看了韦东山老师的数码相框项目,断断续续学完了,现在再整理回顾,做个笔记记录一下。
项目需求:
实现在开发板上显示、浏览图片文件,并能进行图片的放大、缩小、移动、连播等操作
项目的主体框架:
项目的主要流程:
硬件准备
Linux开发板、触摸屏
我的开发板是IMX6ULL,带有4.3英寸触摸屏
主函数分析
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <config.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <encoding_manager.h>
#include <fonts_manager.h>
#include <disp_manager.h>
#include <input_manager.h>
#include <pic_operation.h>
#include <render.h>
#include <picfmt_manager.h>/* main <freetype_file> */
int main(int argc, char **argv)
{ int ret;if (argc != 2){printf("Usage:\n");printf("%s <freetype_file>\n", argv[0]);return 0;}/* 注册显示设备 */DisplayInit();/* 可能可支持多个显示设备: 选择和初始化指定的显示设备 */SelectAndInitDefaultDispDev("fb");/* 分配5个VideoMem缓存,用于预先存放要显示的数据,以加快显示 */AllocVideoMem(5);/* 注册输入设备 */InputInit();/* 调用所有输入设备的初始化函数 */AllInputDevicesInit();/* 注册编码模块 */EncodingInit();/* 注册字库模块 */ret = FontsInit();if (ret){printf("FontsInit error!\n");}/* 设置freetype字库所用的文件和字体尺寸 */ret = SetFontsDetail("freetype", argv[1], 24);if (ret){printf("SetFontsDetail error!\n");}/* 注册图片文件解析模块 */PicFmtsInit();/* 注册页面 */PagesInit();/* 运行主页面 */Page("main")->Run(NULL);return 0;
}
当我们打开数码相框应用程序时,应该马上显示主页面,然后就根据用户的操作采取不同的动作响应。这些工作在Page(“main”)->Run(NULL);中完成。这个函数就是调用页面显示函数显示主页面,同时调用获取输入事件函数,当有输入事件产生,则调用相应的处理函数。例如,用户点击浏览模式,显示浏览界面;当点击页面中图片则显示该图片,当点击目录则进入目录并显示目录内容。
我们接下来分析下具体的模块。
模块分析
1、显示模块
首先要使图片在屏幕上显示出来,这个屏幕可以是LCD触摸屏,VGA屏等等,所以需要一个显示模块来进行显示设备的选择、初始化,获得显示设备的参数,如分辨率、像素位数、显存等,获取了这些参数后我们才可以在屏幕的某一位置描画数据。
为了方便扩展,采用面向对象的思想,先建立一个设备管理模块disp_manager,它提供了一个统一的设备结构体和一些操作函数,这个结构体中包含设备的基本信息和操作集,当有新的设备接入时,它需要提供这样一个结构体向管理模块注册。注册的具体实现是管理模块维护一个链表,链表成员是显示设备的相关结构体,当有显示设备注册时,将其提供的结构体加入链表,这样当选择显示设备时,遍历链表即可。
/* "disp_manager.h" */
typedef struct DispOpr {char *name; // 显示模块的名字int iXres; // X分辨率int iYres; // Y分辨率 int iBpp; // 像素位数 int iLineWidth; // 行字节数 unsigned char *DispMem; // 显存地址 int (*DeviceInit)(void); // 设备初始化函数 int (*ShowPixel)(int iPenX, int iPenY, unsigned int dwColor); // 指定像素显示颜色 int (*CleanScreen)(unsigned int dwBackColor); // 清屏为某颜色 int (*ShowPage)(PT_VideoMem ptVideoMem); // 显示一页struct DispOpr *ptNext;
}T_DispOpr, *PT_DispOpr;
#include <config.h>
#include <disp_manager.h>
#include <string.h>static PT_DispOpr g_ptDispOprHead;
static PT_DispOpr g_ptDefaultDispOpr;
static PT_VideoMem g_ptVideoMemHead;/* 注册某个显示模块,加入到链表中(链表中存放已经注册的显示设备)*/
int RegisterDispOpr(PT_DispOpr ptDispOpr)
{PT_DispOpr tmp;/* 当前链表中尚未有显示设备 */if (!g_ptDispOprHead){g_ptDispOprHead = ptDispOpr;ptDispOpr->ptNext = NULL;}else{/*链表中已有一些显示设备,在链表尾加入即可 */tmp = g_ptDispOprHead;while (tmp->ptNext){tmp = tmp->ptNext;}tmp->ptNext = ptDispOpr;ptDispOpr->ptNext = NULL;}return 0;
}
disp_manager提供了一些统一的操作函数:取出某个指定的显示设备结构体、获得设备基本信息、获得设备显存等
/* 根据名字取出指定的显示设备模块 */
PT_DispOpr GetDispOpr(char *pcName)
{PT_DispOpr tmp = g_ptDispOprHead;while (tmp){if (strcmp(tmp->name, pcName) == 0){return tmp;}tmp = tmp->ptNext;}return NULL;
}
/* 根据名字取出指定的显示设备模块,并调用初始化函数,设为默认设备*/
void SelectAndInitDefaultDispDev(char *name)
{g_ptDefaultDispOpr = GetDispOpr(name);if (g_ptDefaultDispOpr){g_ptDefaultDispOpr->DeviceInit();g_ptDefaultDispOpr->CleanScreen(0);}
}
/* 获取显示设备的分辨率和Bpp */
int GetDispResolution(int *piXres, int *piYres, int *piBpp)
{if (g_ptDefaultDispOpr){*piXres = g_ptDefaultDispOpr->iXres;*piYres = g_ptDefaultDispOpr->iYres;*piBpp = g_ptDefaultDispOpr->iBpp;return 0;}else{return -1;}
}
/* typedef struct VideoMem {int iID; // 标识不同的页面,主页面、浏览页面...int bDevFrameBuffer; // 1标识显存; 0标识普通缓存E_VideoMemState eVideoMemState; // VideoMem本身状态E_PicState ePicState; // VideoMem中内存里图片的状态T_PixelDatas tPixelDatas; // 存储数据struct VideoMem *ptNext;
}T_VideoMem, *PT_VideoMem;
*/
/* 获得显存 */
PT_VideoMem GetDevVideoMem(void)
{PT_VideoMem tmp = g_ptVideoMemHead;while (tmp){if (tmp->bDevFrameBuffer){return tmp;}tmp = tmp->ptNext;}return NULL;
}
在显示过程中,如果要切换显示页面,直接获取数据在向显存中写入效率会很低,画面会有明显的卡顿,为加快显示速度,我们事先在缓存中构造好显示的页面的数据存入VideoMem,显示时再把VideoMem中的数据复制到设备的显存上 。因此可以预先准备多个VideoMem,提供一个分配VideoMem的函数。
/* VideoMem的状态:* 空闲* 用于预先准备显示内容* 用于当前线程*/
typedef enum {VMS_FREE = 0,VMS_USED_FOR_PREPARE,VMS_USED_FOR_CUR,
}E_VideoMemState;/* VideoMem中内存里图片的状态:* 空白* 正在生成* 已经生成*/
typedef enum {PS_BLANK = 0,PS_GENERATING,PS_GENERATED,
}E_PicState;int AllocVideoMem(int iNum)
{int i;int iXres = 0;int iYres = 0;int iBpp = 0;int iVMSize;int iLineBytes;PT_VideoMem ptNew;GetDispResolution(&iXres, &iYres, &iBpp);iVMSize = iXres * iYres * iBpp / 8;iLineBytes = iXres * iBpp / 8;/* 先把设备本身的framebuffer放入链表将tPixelDatas.aucPixelDatas指向显示设备的framebuffer */ptNew = malloc(sizeof(T_VideoMem));if (ptNew == NULL){return -1;}ptNew->tPixelDatas.aucPixelDatas = g_ptDefaultDispOpr->pucDispMem;ptNew->iID = 0;ptNew->bDevFrameBuffer = 1; //表明设备本身的FramebufferptNew->eVideoMemState = VMS_FREE; ptNew->ePicState = PS_BLANK;ptNew->tPixelDatas.iWidth = iXres;ptNew->tPixelDatas.iHeight = iYres;ptNew->tPixelDatas.iBpp = iBpp;ptNew->tPixelDatas.iLineBytes = iLineBytes;ptNew->tPixelDatas.iTotalBytes = iVMSize;/* 如果下面要分配用于缓存的VideoMem, * 把设备本身framebuffer对应的VideoMem状态设置为VMS_USED_FOR_CUR,* 表示这个VideoMem不会被作为缓存分配出去*/if (iNum != 0){ptNew->eVideoMemState = VMS_USED_FOR_CUR;}/* 放入链表 */ptNew->ptNext = g_ptVideoMemHead;g_ptVideoMemHead = ptNew;/* 分配用于缓存的VideoMem */for (i = 0; i < iNum; i++){/* 分配T_VideoMem结构体本身和"跟framebuffer同样大小的缓存"* 即T_VideoMem结构体后面紧跟着数据缓存区,这样ptNew+1即是数据缓冲区地址*/ptNew = malloc(sizeof(T_VideoMem) + iVMSize);if (ptNew == NULL){return -1;}ptNew->tPixelDatas.aucPixelDatas = (unsigned char *)(ptNew + 1);ptNew->iID = 0;ptNew->bDevFrameBuffer = 0;ptNew->eVideoMemState = VMS_FREE;ptNew->ePicState = PS_BLANK;ptNew->tPixelDatas.iWidth = iXres;ptNew->tPixelDatas.iHeight = iYres;ptNew->tPixelDatas.iBpp = iBpp;ptNew->tPixelDatas.iLineBytes = iLineBytes;ptNew->tPixelDatas.iTotalBytes = iVMSize;/* 放入链表 */ptNew->ptNext = g_ptVideoMemHead;g_ptVideoMemHead = ptNew;}return 0;
}
还有一些其他的函数就不一一介绍了,具体见源码。
这个项目我们用到的显示设备是触摸屏,因此需要提供一个触摸屏设备的结构体。
fb.c
#include <config.h>
#include <disp_manager.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <string.h>static int FBDeviceInit(void);
static int FBShowPixel(int iX, int iY, unsigned int dwColor);
static int FBCleanScreen(unsigned int dwBackColor);
static int FBShowPage(PT_VideoMem ptVideoMem);static int fd;static struct fb_var_screeninfo FBVar;
static struct fb_fix_screeninfo FBFix;
static unsigned char *FBMem;
static unsigned int ScreenSize;static unsigned int LineWidth;
static unsigned int PixelWidth;static T_DispOpr FBOpr = {.name = "fb",.DeviceInit = FBDeviceInit,.ShowPixel = FBShowPixel,.CleanScreen = FBCleanScreen,.ShowPage = FBShowPage,
};
/* 初始化fb */
static int FBDeviceInit(void)
{int ret;fd = open(FB_DEVICE_NAME, O_RDWR);if (0 > fd){printf("can't open %s\n", FB_DEVICE_NAME);}//获取可变参数ret = ioctl(fd, FBIOGET_VSCREENINFO, &FBVar);if (ret < 0){printf("can't get fb's var\n");return -1;}//获取固定参数ret = ioctl(fd, FBIOGET_FSCREENINFO, &FBFix);if (ret < 0){printf("can't get fb's fix\n");return -1;}//计算屏幕字节大小ScreenSize = FBVar.xres * FBVar.yres * FBVar.bits_per_pixel / 8;//内存映射FBMem = (unsigned char *)mmap(NULL , ScreenSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (0 > FBMem) {printf("can't mmap\n");return -1;}FBOpr.iXres = FBVar.xres;FBOpr.iYres = FBVar.yres;FBOpr.iBpp = FBVar.bits_per_pixel;FBOpr.iLineWidth = FBVar.xres * FBOpr.iBpp / 8;FBOpr.DispMem = FBMem;LineWidth = FBVar.xres * FBVar.bits_per_pixel / 8;PixelWidth = FBVar.bits_per_pixel / 8;return 0;
}/* 指定fb上的某一像素为指定颜色 */
static int FBShowPixel(int iX, int iY, unsigned int dwColor)
{unsigned char *pucFB;unsigned short *pwFB16bpp;unsigned int *pdwFB32bpp;unsigned short wColor16bpp;int iRed;int iGreen;int iBlue;if ((iX >= FBVar.xres) || (iY >= FBVar.yres)){printf("out of region\n");return -1;}pucFB = FBMem + LineWidth * iY + PixelWidth * iX;pwFB16bpp = (unsigned short *)pucFB;pdwFB32bpp = (unsigned int *)pucFB;//根据像素位数,构建RGBswitch (FBVar.bits_per_pixel){case 8:{*pucFB = (unsigned char)dwColor;break;}case 16:{iRed = (dwColor >> (16+3)) & 0x1f;iGreen = (dwColor >> (8+2)) & 0x3f;iBlue = (dwColor >> 3) & 0x1f;wColor16bpp = (iRed << 11) | (iGreen << 5) | iBlue;*pwFB16bpp = wColor16bpp;break;}case 32:{*pdwFB32bpp = dwColor;break;}default :{printf("can't support %d bpp\n", FBVar.bits_per_pixel);return -1;}}return 0;
}/* 显示一整页 */
static int FBShowPage(PT_VideoMem ptVideoMem)
{memcpy(FBOpr.DispMem, ptVideoMem->tPixelDatas.aucPixelDatas, ptVideoMem->tPixelDatas.iTotalBytes);return 0;
}/* 清屏 */
static int FBCleanScreen(unsigned int dwBackColor)
{unsigned char *pucFB;unsigned short *pwFB16bpp;unsigned int *pdwFB32bpp;unsigned short wColor16bpp;int iRed;int iGreen;int iBlue;int i = 0;pucFB = FBMem;pwFB16bpp = (unsigned short *)pucFB;pdwFB32bpp = (unsigned int *)pucFB;switch (FBVar.bits_per_pixel){case 8:{memset(FBMem, dwBackColor, ScreenSize);break;}case 16:{iRed = (dwBackColor >> (16+3)) & 0x1f;iGreen = (dwBackColor >> (8+2)) & 0x3f;iBlue = (dwBackColor >> 3) & 0x1f;wColor16bpp = (iRed << 11) | (iGreen << 5) | iBlue;while (i < ScreenSize){*pwFB16bpp = wColor16bpp;pwFB16bpp++;i += 2;}break;}case 32:{while (i < ScreenSize){*pdwFB32bpp = dwBackColor;pdwFB32bpp++;i += 4;}break;}default :{printf("can't support %d bpp\n", FBVar.bits_per_pixel);return -1;}}return 0;
}/* 注册FB设备 */
int FBInit(void)
{return RegisterDispOpr(&FBOpr);
}
2、输入设备模块
输入模块主要是注册输入设备,并且获取输入设备的输入事件,返回输入的数据。与上面一样,为了便于扩展,它也提供了一个管理模块,提供统一的输入设备结构体和注册、操作函数,输入设备实现此结构体并向输入管理模块注册。
typedef struct InputOpr {char *name; //输入模块名pthread_t tTreadID; //子线程IDint (*DeviceInit)(void); int (*DeviceExit)(void); int (*GetInputEvent)(PT_InputEvent ptInputEvent); //获取输入数据struct InputOpr *ptNext;
}T_InputOpr, *PT_InputOpr;
输入设备当接收到输入事件时,他需要知道这个输入的具体信息,比如输入事件类型、产生时间、输入事件产生在哪个位置,对于触摸屏而言即X、Y坐标;压力值(是否按下);按键值等等。因此需要定义一个存储这些信息的结构体。
// 输入事件类型
#define INPUT_TYPE_STDIN 0
#define INPUT_TYPE_TOUCHSCREEN 1
...typedef struct InputEvent {struct timeval tTime; //时间int iType; //类型int iX; //X坐标int iY; //Y坐标int iKey; //按键值int iPressure; //压力值
}T_InputEvent, *PT_InputEvent;
注册函数:
/* 注册单个输入模块 */
int RegisterInputOpr(PT_InputOpr ptInputOpr)
{PT_InputOpr ptTmp;if (!g_ptInputOprHead){g_ptInputOprHead = ptInputOpr;ptInputOpr->ptNext = NULL;}else{ptTmp = g_ptInputOprHead;while (ptTmp->ptNext){ptTmp = ptTmp->ptNext;}ptTmp->ptNext = ptInputOpr;ptInputOpr->ptNext = NULL;}return 0;
}
初始化输入设备并创建一个线程来获取输入事件
/* 初始化输入设备,并创建用于读取设备输入数据的子线程 */
int AllInputDevicesInit(void)
{PT_InputOpr ptTmp = g_ptInputOprHead;int ret = -1;while (ptTmp){if (0 == ptTmp->DeviceInit()){/* 创建子线程 */pthread_create(&ptTmp->tTreadID, NULL, InputEventThreadFunction, ptTmp->GetInputEvent); ret = 0;}ptTmp = ptTmp->ptNext;}return ret;
}
/* 读取设备输入数据的子线程 */
static void *InputEventThreadFunction(void *pVoid)
{T_InputEvent tInputEvent;/* 定义函数指针 *//* pVoid: ptTmp->GetInputEvent ,具体输入设备的获取输入事件函数 */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 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;
}
因为我们用的是触摸屏,所以注册一个触摸屏设备即可
touchscreen.c
#include <config.h>
#include <input_manager.h>
#include <disp_manager.h>
#include <stdlib.h>
//利用tslib库
#include <tslib.h>static struct tsdev *g_tTSDev;
static int giXres;
static int giYres;/* 初始化 */
static int TouchScreenDevInit(void)
{char *pcTSName = NULL;int iBpp;if ((pcTSName = getenv("TSLIB_TSDEVICE")) != NULL ) {g_tTSDev = ts_open(pcTSName, 0); /* 以阻塞方式打开 */}else{g_tTSDev = ts_open("/dev/event0", 1);}if (!g_tTSDev) {printf(A"ts_open error!\n");return -1;}if (ts_config(g_tTSDev)) {printf("ts_config error!\n");return -1;}if (GetDispResolution(&giXres, &giYres, &iBpp)){return -1;}return 0;
}/* 读取数据 */
static int TouchScreenGetInputEvent(PT_InputEvent ptInputEvent)
{struct ts_sample tSamp;int iRet; while (1){/* #include <tslib.h>int ts_read(struct tsdev *dev, struct ts_sample *samp, int nr);*/iRet = ts_read(g_tTSDev, &tSamp, 1); /* 如果无数据则休眠 */if (iRet == 1){ptInputEvent->tTime = tSamp.tv;ptInputEvent->iType = INPUT_TYPE_TOUCHSCREEN;ptInputEvent->iX = tSamp.x;ptInputEvent->iY = tSamp.y;ptInputEvent->iPressure = tSamp.pressure;return 0;}else{return -1;}}return 0;
}/* 退出触摸屏模块 */
static int TouchScreenDevExit(void)
{return 0;
}static T_InputOpr g_tTouchScreenOpr = {.name = "touchscreen",.DeviceInit = TouchScreenDevInit,.DeviceExit = TouchScreenDevExit,.GetInputEvent = TouchScreenGetInputEvent,
};/* 注册 */
int TouchScreenInit(void)
{return RegisterInputOpr(&g_tTouchScreenOpr);
}