之前看了韦东山老师的数码相框项目,断断续续学完了,现在再整理回顾,做个笔记记录一下。
项目需求:
实现在开发板上显示、浏览图片文件,并能进行图片的放大、缩小、移动、连播等操作
项目的主体框架:
项目的主要流程:
硬件准备
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; //表明设备本身的Framebuffer
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;
/* 如果下面要分配用于缓存的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;
//根据像素位数,构建RGB
switch (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; //子线程ID
int (*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(APP_ERR"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);
}