嵌入式Linux项目-电子产品量产工具

 声明:项目来自韦东山老师百问网嵌入式专家-韦东山嵌入式专注于嵌入式课程及硬件研发 (100ask.net)icon-default.png?t=N7T8https://www.100ask.net/video/detail/p_5f04515ce4b036f1c0cf4254

GIT下载代码,如下所示:


$ git clone https://e.coding.net/weidongshan/01_all_series_quickstart.git

1. 项目简介

本项目有如下特点:

         ① 简单易用: 把这套软件烧写在SD卡上,插到IMX6ULL板子里并启动,它就会自动测试各个模块、烧写EMMC系统。 工人只要按照说明接入几个模块,就可以完成整个测试、烧写过程。 测试结果一目了然:等LCD上所有模块的图标都变绿时,就表示测试通过。

         ② 软件可配置、易扩展: 通过配置文件添加测试项,可以添加不限个数的测试项。 每个测试项有自己的测试程序,测试通过后把结果发送给GUI即可。各个测试程序互不影响。

         ③ 纯C语言编程

2. 项目框架       

        一个项目拆分成各个子系统;面向对象编程、模块化编程,对于一个功能构造抽抽象一个结构体,每个结构体中有个指针域指向下一个结构体,便于管理。该项目主要包含以下几个部分:显示系统、输入系统、文字系统、UI系统、页面系统、业务系统;

  

 显示系统:基于framebuffer,打开LCD设备节点,获取分辨率等参数,映射framebuffer,实现描点函数等;

输入系统:触摸屏输入、网络输入,基于tslib库和udp协议,使用环形缓冲区保存、管理数据;创建多线程,支持多个输入设备;

文字系统:基于Freetype库开发,实现矢量字符串,表示检测模块名称;

UI系统:在LCD上绘制各模块按钮

业务系统:调用各模块接口;处理配置文件、生成界面、处理输入事件等

页面系统:实现页面注册、页面管理

3. 源码分析

3.1 显示系统

  1. 抽象显示系统数据结构体,---模块化编程,对于一个功能构造一个结构体
  2. 底层功能模块填充结构体,---可实现多个显示模块
  3. 注册底层结构体到链表,遍历链表,抽象出显示系统API接口函数

3.1.1 抽象显示系统数据结构体

        使用一个结构体描述一个显示设备,指针域便于注册、管理多个结构体

        使用一个结构体封装显示屏参数

typedef struct DispOpr {
  	     //数据域
  	char *name;
  	int (*Device_init)(void);
  	int (*Device_exit)(void);      
    int (*GetBuffer)(PDispBuff ptDispBuff);//返回值判断正误,数据保存在ptDispBuffer结构体中
  	int (*FlushRegion)(PRegion ptRegion, PDispBuff ptDispBuff);//刷区域
		//指针域,指向下一个结构体的首地址
	struct DispOpr *ptNext;
}DispOpr, *PDispOpr ;

//buffer分辨率- 多长多宽,像素占多少位
typedef struct DispBuff {
	int iXres;
	int iYres;
	int iBpp;
	char *buff;
}DispBuff, *PDispBuff;

typedef在这里做了两件事:

  1. 它定义了DispOpr作为struct DispOpr的别名。这意味着在代码中,你可以使用DispOpr来声明这种类型的变量,而不必每次都写出struct DispOpr

  2. 它还定义了PDispOpr作为指向DispOpr类型的指针的别名。这样,你就可以使用PDispOpr来声明指向DispOpr类型变量的指针,而不必写出struct DispOpr*

3.1.2 Framebuffer

        在Linux 系统中通过Framebuffer驱动程序来控制LCD。Frame是帧的意思,buffer是缓冲的意思,这意味着Framebuffer就是一块内存,里面保存着 一帧图像。Framebuffer 中保存着一帧图像的每一个像素颜色值,假设 LCD 的 分辨率是1024x768,每一个像素的颜色用32位来表示,那么Framebuffer的 大小就是:1024x768x32/8=3145728字节。

        简单介绍LCD的操作原理:

  1. 驱动程序设置好LCD控制器: 根据LCD的参数设置LCD控制器的时序、信号极性; 根据LCD分辨率、BPP分配Framebuffer。
  2. APP使用ioctl获得LCD分辨率、BPP
  3. APP通过mmap映射Framebuffer,在Framebuffer中写入数据

        本项目显示系统通过Framebuffer(帧缓存)实现,Framebuffer作为底层功能模块,只需要填充显示系统数据结构体,由上层程序管理。 

static DispOpr g_tFramebufferopr = {
	.name       = "fd",
	.GetBuffer  =FdGetbuffer,//用于获得buffer设定图案形状
	.FlushRegion=FDFlushRegion,//将buffer刷到屏幕
	.Device_init=FDdevice_init,
	.Device_exit=FDdevice_exit,
};

name:用于标识显示系统,便以管理、遍历

FDdevice_init():打开设备节点、获取屏幕参数、mmap映射到用户空间;

static int FDdevice_init(void)
{
	fd_fb = open("/dev/fb0", O_RDWR);
	if (fd_fb == -1) {
		perror("open");
		return -1;
	}
	//ioctl:获取可变屏幕信息(variable screen information),把获取到的屏幕信息存储到这个结构体中。
	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
	{
		perror("ioctl");
		close(fd_fb);
		return -1;
	}
	//ioctl获得屏幕信息保存在var结构体中,通过var结构体算出,每行的字节数 (line_width)
	//每个像素的字节数 (pixel_width),整个屏幕的字节数 (screen_size)
	line_width	= var.xres * var.bits_per_pixel / 8;
	pixel_width = var.bits_per_pixel / 8;
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
	//使用mamp将文件或设备映射到用户空间,使文件内容可以通过内存地址来访问。
	//mmap 返回 void * 指针,通过将其转换为 unsigned char * 可以方便地按字节操作映射的内存区域。
	fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
	if (fb_base == (unsigned char *)-1)
	{
		perror("fb_base");
		close(fd_fb);
		return -1;
	}
	return 0;
}

FDdevice_exit():取消映射、关闭设备节点;

static int FDdevice_exit(void)
{
	munmap(fb_base, screen_size);
	close(fd_fb);
	return 0;
}

FdGetbuffer():将显示屏数据保存在结构体

static int FdGetbuffer(PDispBuff ptDispBuffer)//返回值判断正误,数据保存在ptDispBuffer结构体中
{
	ptDispBuffer->iXres = var.xres;//用于接收屏幕的水平分辨率(像素数)。
	ptDispBuffer->iYres = var.yres;
	ptDispBuffer->iBpp  = var.bits_per_pixel;
	ptDispBuffer->buff  =(char *)fb_base;
	return 0;
}

FDFlushRegion():空!本项目通过描点函数绘制图案

将g_tFramebufferopr结构体注册进链表

void RegisterFramebuffer(void)
{
 	RegisterDisplay (&g_tFramebufferopr);
}

3.1.3 显示系统管理器

        承上启下、注册底层模块结构体、遍历链表、提供API接口

  1. 1、注册显示设备结构体,链表(头插法)
  2. 2、将framebuffer注册进链表
  3. 3、根据“name”选择默认显示器
  4. 4、初始化默认显示器
  5. 5、获得显示设备buffer
  6. 6、刷区域     ---描点函数:使用描点函数实现显示

注册显示系统结构体,链表(头插法)

void RegisterDisplay (PDispOpr PtDispOpr)// 注册结构体
{
		/* 头插法 头结点g_DispDevs为一个指针,指向下一个节点首地址*/
	PtDispOpr->ptNext = g_DispDevs;
	g_DispDevs = PtDispOpr;
}

将framebuffer注册进链表

void Registerdevice(void)
{
	extern void RegisterFramebuffer(void);
	RegisterFramebuffer();
}

根据“name”选择默认显示器:遍历链表找到“name”显示模块

int SelectDefaultDisplay(char*name)//根据名字找到默认显示器
{
	PDispOpr pTem =g_DispDevs;
	while(pTem)//pTem不空比较名字
	{
		if(strcmp(pTem->name,name)==0)//返回0比较成功
		{
			g_DisDefault=pTem;//返回给全局变量
			return 0;
		}
		pTem=pTem->ptNext;
	}
	return -1;
}

        初始化默认显示器:调用底层指针函数,并获得显示系统buffer

int InitDefaultDisplay(void)//初始化默认显示器
{
	int ret;
	ret=g_DisDefault->Device_init();
	if(ret)
	{
		printf("Device_init err\n");
		return -1;
	}
	ret=g_DisDefault->GetBuffer(&g_tDispBuff);
	if(ret)
	{
		printf("GetBuffer err\n");
		return -1;
	}
	line_width=g_tDispBuff.iXres * g_tDispBuff.iBpp/8;
	pixel_width=g_tDispBuff.iBpp/8;
	return 0;
}

    显示器刷区域函数为空便于扩展所以写出

     本项目使用描点函数实现显示

void putpixel(int x, int y, unsigned int dwcolor)//dwcolor:double word
{
	unsigned char *pen_8 = (unsigned char *)(g_tDispBuff.buff+y*line_width+x*pixel_width);//计算像素在帧缓冲区中的位置
	unsigned short *pen_16;	
	unsigned int *pen_32;	
	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;

	switch (g_tDispBuff.iBpp)
	{
		case 8:
		{
			*pen_8 = dwcolor;
			break;
		}
		case 16:
		{
			/* 565 */
			red   = (dwcolor >> 16) & 0xff;
			green = (dwcolor >> 8) & 0xff;
			blue  = (dwcolor >> 0) & 0xff;
			dwcolor = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = dwcolor;
			break;
		}
		case 32:
		{
			*pen_32 = dwcolor;
			break;
		}
		default:
		{
			printf("can't surport %dbpp\n", g_tDispBuff.iBpp);
			break;
		}
	}
  1.  传入的dwcolor表示颜色,它的格式永远是0x00RRGGBB,即RGB888。 当LCD是16bpp时,要把color变量中的R、G、B抽出来再合并成RGB565格 式。
  2. 计算(x,y)坐标上像素对应的Framebuffer地址。
  3. 对于8bpp,dwcolor就不再表示RBG三原色了,这涉及调色板的概 念,dwcolor是调色板的值。 
  4. 对于16bpp先从dwcolor变量中把R、G、B抽出来。 把red、green、blue这三种8位颜色值,根据RGB565的格式, 只保留red中的高5位、green中的高6位、blue中的高5位,组合成一个新 的16位颜色值。把新的16位颜色值写入Framebuffer。
  5. 对于32bpp,颜色格式跟color参数一致,可以直接写入 Framebuffer。

3.2 输入系统

        在本项目中输入设备有两个:触摸屏输入、网络输入。

  1.  如led灯需操作人员观察是否闪烁,点击led按钮,它变成绿色表示测试通过;
  2.  对于其他设备如AP3216C,由脚本自检没有问题,它会向指定的IP地址发送设备正常运行的通知,当通过网络收到该通知,对应按钮变成绿色表示测试通过;

        输入系统框架:

  1. 抽象输入系统数据结构体,---模块化编程,对于一个功能构造一个结构体
  2. 底层功能模块填充结构体,---实现触摸屏输入和网络输入
  3. 注册底层结构体到链表,遍历链表,抽象出输入系统API接口函数

3.2.1 抽象输入系统结构体 

用一个结构体封装指针函数描述一个输入设备;

用一个结构体描述一个输入事件,用于保存输入事件数据;

typedef struct InputDevice{
	char *name ;
	int (*GetInputEvent)(PInputEvent PtInputEvent);
	int (*DeviceInit)(void);
	int (*DeviceExit)(void);
	struct InputDevice *ptNext;
}InputDevice,*PInputDevice;

typedef struct InputEvent {
	struct timeval tTime;
	int iType;
	/*触摸屏事件*/
	int iX;
	int iY;
	int iPressure;
	/*网络输入事件*/
	char str[1024];
}InputEvent,*PInputEvent;

3.2.2 触摸屏输入

        触摸屏输入事件基于tslib库,tslib 是一个触摸屏的开源库,可以使用它来访问触摸屏设备

使用tslib库很简单,在开发板交叉编译tslib库后,调用几个接口函数就能读取触摸屏数据;

  1. 1、打开设备节点
  2. 2、读取触摸屏事件
  3. 3、关闭设备节点

       实现基于触摸屏输入事件的结构体、并注册进链表

static InputDevice g_tTouchscreenDev={
	.name          ="touchscreen",
	.GetInputEvent =TouchscreenGetInputEvent,
	.DeviceInit    =TouchscreenDeviceInit,
	.DeviceExit    =TouchscreenDeviceExit,
};

void TouchscreenRegister(void)
{
	RegisterInputDevice(&g_tTouchscreenDev);
}

TouchscreenDeviceInit:

ts_setup()函数会调用ts_open,打开设备节点,构造出一个tsdev结构体。然后调用ts_config读取配置文件的处理

static int TouchscreenDeviceInit (void)
{
	/*打开配置设备,文件名;NULL,0:非阻塞*/
	g_ts = ts_setup(NULL, 0);
	if (!g_ts)
	{
		printf("ts_setup err\n");
		return -1;
	}
	
	return 0;
}

TouchscreenGetInputEvent :该触摸屏读取单点值就可,调用ts_read函数,读取触摸屏单点值,然后将获得触摸屏数据保存到输入事件结构体中;

static int TouchscreenGetInputEvent (PInputEvent ptInputEvent)
{
	struct ts_sample samp;
	int ret;
	
	/*读g_ts设备,保存在samp中,读一个字节*/
	ret = ts_read(g_ts, &samp, 1);
	
	if (ret != 1)
		return -1;
	ptInputEvent->iType     = INPUT_TYPE_TOUCH;
	ptInputEvent->iX        = samp.x;
	ptInputEvent->iY        = samp.y;
	ptInputEvent->iPressure = samp.pressure;
	ptInputEvent->tTime     = samp.tv;
	return 0;
}

关闭设备节点

static int TouchscreenDeviceExit(void)
{
	ts_close(g_ts);
	return 0;
}

3.2.3 网络输入

        网络输入基于UDP协议, 程序相当于server端,等client端发消息,使用socket标准接口实现

实现基于网络输入的输入事件结构体、并注册进链表

static InputDevice g_tNetinputDev={
	.name          ="net",
	.GetInputEvent = NetGetInputEvent,
	.DeviceInit    = NetDeviceInit,
	.DeviceExit    = NetDeviceExit,
};

void NetInputRegister(void)
{
	RegisterInputDevice(&g_tNetinputDev);
}

函数解析;

        static int NetDeviceInit (void):

  1. 1、创建一个套接字,初始化
  2. 2、将地址绑定到一个套接字
  3. 使用udp协议不用建立面向连接
static int NetDeviceInit (void)
{
	int ret;
	struct sockaddr_in server_sockaddr;
	server_socket_fd =socket(AF_INET, SOCK_DGRAM,0);//创建一个套接字,AF_INET 是针对 Internet,SOCK_STREAM 表明用的是 TCP 协议
	if(server_socket_fd<0)
	{
		printf("socket err\n");
		return -1;
	}
	server_sockaddr.sin_addr.s_addr =INADDR_ANY;
	server_sockaddr.sin_family      =AF_INET;
	server_sockaddr.sin_port        =htons(SERVER_PORT);  /* host to net, short */
	memset(server_sockaddr.sin_zero, 0, 8);// 用于将内存块设置为特定值的标准库函数:将 server_sockaddr 结构体的 sin_zero 字段中的前 8 个字节设置为 0。
	ret=bind(server_socket_fd, (const struct sockaddr *)&server_sockaddr, sizeof(struct sockaddr));//将地址绑定到一个套接字,bind 函数的第二个参数应该是指向 const struct sockaddr * 的指针。
	if(ret<0)
	{
		printf("bind err\n");
		return -1;
	}
	return 0;
}

NetGetInputEvent:

接受客户端发来的信息,并将其保存在输入事件结构体中

static int  NetGetInputEvent (PInputEvent ptInputEvent)
{
	unsigned int iAddrLen;
	int iRecvLen;
	unsigned char ucRecvBuf[1000];
	struct sockaddr_in client_sockaddr;
	iAddrLen = sizeof(struct sockaddr);
	
	iRecvLen= recvfrom(server_socket_fd, ucRecvBuf, 999, 0, (struct sockaddr *)&client_sockaddr, &iAddrLen);
	if(iRecvLen==-1)
	{
		printf("recvfrom err\n");
		return -1;
	}
	else
	{
		ucRecvBuf[iRecvLen] = '\0';
		//printf("Get Msg From %s : %s\n", inet_ntoa(client_sockaddr.sin_addr), ucRecvBuf);
		ptInputEvent->iType=INPUT_TYPE_NET;
		gettimeofday(&ptInputEvent->tTime, NULL);//获取当前时间并将其存储在 ptInputEvent->tTime 中
		memcpy(ptInputEvent->str,ucRecvBuf,sizeof(ucRecvBuf));
		ptInputEvent->str[iRecvLen]='\0';
	}

	return 0;
}

关闭socket接口

static int  NetDeviceExit(void)
{
	close(server_socket_fd);
	return 0;
}

3.2.4 环形缓冲区

        触摸屏输入可能一次上报多组数据, 网络输入可能多个client发来数据,不可使用单一变量保存数据,所以使用数组保存即环形缓冲区

        定义写指针、读指针、缓存区

#define BUFFER_LEN 20
static int g_iRead;
static int g_iWrite;
static InputEvent g_atInputEvents[BUFFER_LEN];//缓存区

        环形缓冲区中最重要两个状态:缓冲区满、缓冲区空

缓冲区满:写指针的下一个位置等于读指针的位置。

缓冲区空:读指针等于写指针的位置。

static int isInputBufferFull(void)
{
	return (g_iRead == ((g_iWrite + 1) % BUFFER_LEN));
}

static int isInputBufferEmpty(void)
{
	return (g_iRead == g_iWrite);
}

当缓冲区非满时可以写缓冲区,将输入事件结构体中的值写入缓冲区

当缓冲区非空时可以读缓冲区,将缓冲区中读位置值读取出

static void PutInputEventToBuffer (PInputEvent ptInputEvent)
{
	if(!isInputBufferFull())
	{
		g_atInputEvents[g_iWrite]=*ptInputEvent;//将 ptInputEvent 指向的结构体对象的值复制到缓冲区中
		g_iWrite=(g_iWrite+1)%BUFFER_LEN;
	}
}
static int GetInputEventFromBuffer (PInputEvent ptInputEvent)
{
	if(!isInputBufferEmpty())
	{
		*ptInputEvent=g_atInputEvents[g_iRead];
		g_iRead=(g_iRead+1)%BUFFER_LEN;
		return -1;
	}
	else 
	{
		return 0;
	}
}

3.2.5 输入系统管理器

       将底层模块结构体注册进链表,遍历链表,初始化所有输入设备,获得输入数据;

注册进链表、注册底层模块

void RegisterInputDevice (PInputDevice ptInputDevice)
{		/*头插法*/
	ptInputDevice->ptNext=g_InputDevs;
	g_InputDevs=ptInputDevice;//表头指向新结构体
}

	void InputInit (void)
{
		/* regiseter netinput */
	extern void NetInputRegister(void);
	NetInputRegister();
		/* regiseter touchscreen */
	extern void TouchscreenRegister(void);
	TouchscreenRegister();
	
}

初始化所有输入设备,获得输入数据;

  1. 遍历链表,取出输入设备;
  2. 由于多个输入设备(使用轮询方式会丢失数据);
  3. 避免数据丢失,创建多线程(为每一个输入设备都创建一个线程),子线程中将输入事件数据写入环形缓冲区中;
  4. 由于多线程都需访问环形缓冲区,避免竞争,使用互斥锁,保证同一时间只有一个线程可以访问环形缓冲区;
  5. 主线程读环形缓冲区时缓冲区空,可使用条件变量等待子线程写入缓冲区在读取

代码如下:

InputDeviceInit();遍历链表时,取出输入设备,创建多线程(创建线程时、传入参数)

 int  InputDeviceInit (void)
{
	int ret;
	pthread_t tid;
	/* for each inputdevice, init, pthread_create */
	PInputDevice ptTmp = g_InputDevs;//临时变量ptem指向表头,即遍历整个链表
	while(ptTmp)
	{
		ret=ptTmp->DeviceInit();
		if(ret)
		{
			printf("ptTmp->DeviceInit err\n");
			return -1;
		}
		else
		{
			pthread_create(&tid,NULL,input_recv_thread_func,ptTmp);
		}
		ptTmp=ptTmp->ptNext;
	}
	return 0;

}

static void *input_recv_thread_func (void *data):子线程用于将输入事件写入环形缓冲区(子线程写入缓冲区时,要上锁,写成功后要解锁,发信号)

static void *input_recv_thread_func (void *data)//void *data由主线程传入,即ptTmp
{
	PInputDevice tInputDev = (PInputDevice)data;//从主线程获得结构体
	InputEvent tEvent;
	int ret;
	while(1)
	{
		/* 读数据 */
		ret = tInputDev->GetInputEvent(&tEvent);
		if(!ret)
		{
			/* 保存数据 */
			pthread_mutex_lock(&g_tMutex);
			PutInputEventToBuffer(&tEvent);
			/* 唤醒等待数据的线程 */
			pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
			pthread_mutex_unlock(&g_tMutex);
		}
	}
	return NULL;
}

GetInputDeviceEvent():主线程用于读环形缓存区数据保存到输入事件结构体中(读缓冲区前要获得锁、上锁、读取成功后解锁;无数据休眠等待子线程唤醒)

int  GetInputDeviceEvent (PInputEvent PInputEvent)
{
	
	InputEvent tEvent;
	int ret;
	/* 无数据则休眠 */
	pthread_mutex_lock(&g_tMutex);
	if (GetInputEventFromBuffer(&tEvent))//根据GetInputEventFromBuffer返回值判断,是否有数据
	{
		*PInputEvent = tEvent;
		pthread_mutex_unlock(&g_tMutex);
		return 0;
	}
	else
	{
		/* 休眠等待 */
		pthread_cond_wait(&g_tConVar, &g_tMutex);	
		if (GetInputEventFromBuffer(&tEvent))
		{
			*PInputEvent = tEvent;
			ret = 0;
		}
		else
		{
			ret = -1;
		}
		pthread_mutex_unlock(&g_tMutex);		
	}
	return ret;
}

3.3 文字系统

        使用点阵字库显示英文字母、汉字时,大小固定,如果放大缩小则会模糊甚 至有锯齿出现,为了解决这个问题,引用矢量字体。       

文字系统基于Freetype库开发显示矢量字体,用于表示模块名称;

 3.3.1 抽象文字系统结构体

        抽象出一个结构体描述字体,抽象出一个结构体描述一个字符

/**结构体FontBitMap,能描述一个"字符":位置、大小、位图**/
typedef struct FontBitMap {
	Region tRegion;
	int iCurOriginX;/*位图原点x坐标,一般位于左下方,根据原点确定相邻字符位置*/
	int iCurOriginY;/*位图原点y坐标*/
	int iNextOriginX;/*下一个字符即相邻右侧字符原点X坐标*/
	int iNextOriginY;/*下一个字符即相邻右侧字符原点Y坐标*/
	unsigned char *pucBuffer;/*存有字符的位图数据*/
}FontBitMap, *PFontBitMap;

/** 抽象出一个结构体FontOpr,能描述字体的操作**/
/** 比如Freetype的操作、固定点阵字体的操作**/
typedef struct FontOpr {
	char *name;							/*字体模块名字*/
	int (*FontInit)(char *aFineName);	/*字体模块初始化*/
	int (*SetFontSize)(int iFontSize);	/*设置字体尺寸*/
	int (*GetFontBitMap)(unsigned int dwCode, PFontBitMap ptFontBitMap);/*根据编码值获得字符的位图*/
	int (*FreetypeGetStringRegionCar)(char *str, PRegionCartesian ptRegionCar);//获得字符串
	struct FontOpr *ptNext;
}FontOpr, *PFontOpr;

3.3.2 Freetype

     Freetype 是开源的字体引擎库,它提供统一的接口来访问多种字体格式文件, 从而实现矢量字体显示。我们只需要移植这个字体引擎,调用对应的API接口, 提供字体文件,就可以让freetype库帮我们取出关键点、实现闭合曲线,填充 颜色,达到显示矢量字体的目的。

        freetype 库使用步骤

        1. 初始化:FT_InitFreetype 

FT_Library library;
FT_Init_FreeType(&library);
//   初始化 freetype 库,并创建一个 FT_Library 类型的对象,用于存储库的实例信息。

        2. 加载(打开)字体Face:FT_New_Face   

           

FT_Face face;
FT_New_Face(library, "path/to/font.ttf", 0, &face);
//用于从字体文件中加载一个字体面(Face)

        3. 设置字体大小:FT_Set_Char_Sizes 或 FT_Set_Pixel_Sizes      

        4. 选择charmap:FT_Select_Charmap

FT_Select_Charmap(face, FT_ENCODING_UNICODE);
//用于选择字体的字符映射表(Charmap),将字符编码值映射到字符索引(Glyph Index)
//FT_ENCODING_UNICODE 表示选择Unicode字符编码。

        5. 根据编码值charcode找到glyph_index:glyph_index = FT_Get_Char_Index(face,charcode)

FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
//函数根据给定的字符编码值(如Unicode值)返回对应的字形索引

        6. 根据glyph_index取出glyph:FT_Load_Glyph(face,glyph_index)

FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT);
//函数加载指定字形索引的字形数据(如轮廓、度量信息等)到内存中,准备进行渲染。

        7. 转为位图:FT_Render_Glyph

FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
//函数将加载的字形数据转换为位图格式

        8. 移动或旋转:FT_Set_Transform

FT_Matrix matrix;
FT_Vector pen;
FT_Set_Transform(face, &matrix, &pen);
//函数用于设置字体的变换矩阵和偏移量,以实现移动或旋转效果

        9. 最后显示出来。

上面的5、6、7可以使用一个函数代替:FT_Load_Char(face, charcode, FT_LOAD_RENDER),它就可以得到位图。

本项目基于Freetype字体实现:

填充文字字体结构体,注册进链表

static FontOpr g_tFreeTypeOpr ={

	.name         ="freetype",				
	.FontInit     = FreeTypeFontInit,
	.SetFontSize  =FreeTypeSetFontSize,
	.GetFontBitMap=FreeTypeGetFontBitMap,
	.FreetypeGetStringRegionCar = FreetypeGetStringRegionCar,

};
void RegisterFreeTypeDevice (void)
{
	RegisterfontDevice(&g_tFreeTypeOpr);
}

源码解释:

FreeTypeFontInit:

1、初始化Freetype

2、加载打开字体保存在face

3、设置默认字体大小

int FreeTypeFontInit (char *aFineName)//传入字体文件
{
	int error;
	FT_Library    library;
	error = FT_Init_FreeType( &library );              /* initialize library */
    if(error)
    {
		printf("FT_Init_FreeType err ");
		return -1;
	}
    error = FT_New_Face( library, aFineName, 0, &g_tFace ); /* create face object加载打开字体保存在face */
    if(error)
    {
		printf("FT_New_Face err ");
		return -1;
	}

    FT_Set_Pixel_Sizes(g_tFace, g_iDefaultFontSize, 0);
	return 0;
}

FreeTypeGetFontBitMap:获得字符数据保存在结构体中

int FreeTypeGetFontBitMap (unsigned int dwCode, PFontBitMap ptFontBitMap)
{
	int error;
    FT_Vector pen;//当前字符绘制位置(起点)。
    //FT_Glyph  glyph;//用于存储字符的glyph信息。
    FT_GlyphSlot slot = g_tFace->glyph;//face中获得FT_GlyphSlot,后面的代码中文字的位图就是保存在FT_GlyphSlot 里
    
	/*计算绘制字符串起点的坐标。坐标乘以64因为FreeType使用的是1/64像素单位。*/
	pen.x = ptFontBitMap->iCurOriginX * 64; /* 单位: 1/64像素 */
    pen.y = ptFontBitMap->iCurOriginY * 64; /* 单位: 1/64像素 */
	
 		/* 转换:transformation */
	FT_Set_Transform(g_tFace, 0, &pen);//移动或者旋转

		/* 加载位图: load glyph image into the slot (erase previous one) */
	error = FT_Load_Char(g_tFace, dwCode, FT_LOAD_RENDER);//得到位图,执行 FT_Load_Char 之后,字符的位图被存在 slot->bitmap 里face->glyph->bitmap。
	if (error)
	{
		printf("FT_Load_Char error\n");
		return -1;
	}
	ptFontBitMap->pucBuffer = slot->bitmap.buffer;
	
	ptFontBitMap->tRegion.iLeftUpX = slot->bitmap_left;
	ptFontBitMap->tRegion.iLeftUpY = ptFontBitMap->iCurOriginY*2 - slot->bitmap_top;
	ptFontBitMap->tRegion.iWidth   = slot->bitmap.width;
	ptFontBitMap->tRegion.iHeigh   = slot->bitmap.rows;
	ptFontBitMap->iNextOriginX = ptFontBitMap->iCurOriginX + slot->advance.x / 64;
	ptFontBitMap->iNextOriginY = ptFontBitMap->iCurOriginY;

	return 0;
}

FreetypeGetStringRegionCar:显示字符串

核心:根据Freetype获得单个字体外框。确定字体之间的坐标,计算出外框;(详细计算查看Freetype数据结构)

static int FreetypeGetStringRegionCar(char *str, PRegionCartesian ptRegionCar)
{
    int i;
    int error;
    FT_BBox bbox;
    FT_BBox glyph_bbox;
    FT_Vector pen;
    FT_Glyph  glyph;
    FT_GlyphSlot slot = g_tFace->glyph;

    /* 初始化 */
    bbox.xMin = bbox.yMin = 32000;
    bbox.xMax = bbox.yMax = -32000;

    /* 指定原点为(0, 0) */
    pen.x = 0;
    pen.y = 0;

    /* 计算每个字符的bounding box */
    /* 先translate, 再load char, 就可以得到它的外框了 */
    for (i = 0; i < strlen(str); i++)
    {
        /* 转换:transformation */
        FT_Set_Transform(g_tFace, 0, &pen);

        /* 加载位图: load glyph image into the slot (erase previous one) */
        error = FT_Load_Char(g_tFace, str[i], FT_LOAD_RENDER);
        if (error)
        {
            printf("FT_Load_Char error\n");
            return -1;
        }

        /* 取出glyph */
        error = FT_Get_Glyph(g_tFace->glyph, &glyph);
        if (error)
        {
            printf("FT_Get_Glyph error!\n");
            return -1;
        }
        
        /* 从glyph得到外框: bbox */
        FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_TRUNCATE, &glyph_bbox);

        /* 更新外框 */
        if ( glyph_bbox.xMin < bbox.xMin )
            bbox.xMin = glyph_bbox.xMin;

        if ( glyph_bbox.yMin < bbox.yMin )
            bbox.yMin = glyph_bbox.yMin;

        if ( glyph_bbox.xMax > bbox.xMax )
            bbox.xMax = glyph_bbox.xMax;

        if ( glyph_bbox.yMax > bbox.yMax )
            bbox.yMax = glyph_bbox.yMax;
        
        /* 计算下一个字符的原点: increment pen position */
        pen.x += slot->advance.x;
        pen.y += slot->advance.y;
    }

    /* return string bbox */
    //*abbox = bbox;
    ptRegionCar->iLeftUpX = bbox.xMin;
    ptRegionCar->iLeftUpY = bbox.yMax;
    ptRegionCar->iWidth     = bbox.xMax - bbox.xMin + 1;
    ptRegionCar->iHeigh     = bbox.yMax - bbox.yMin + 1;
	return 0;	
}

3.3.3 文字系统管理器

        注册Freetype结构体,遍历链表(实现过程同上)

        取出Freetype,调用函数指针(Freetype功能函数较完善,上层只需抽象出函数)

int SetFontSize(int iFontSize)
{
	return g_ptDefaulFontOpr->SetFontSize(iFontSize);
}

int GetFontBitMap(unsigned int dwCode, PFontBitMap ptFontBitMap)
{
	return g_ptDefaulFontOpr->GetFontBitMap(dwCode, ptFontBitMap);
}

int GetStringRegionCar(char *str, PRegionCartesian ptRegionCar)
{
	return g_ptDefaulFontOpr->FreetypeGetStringRegionCar(str, ptRegionCar);
}

本项目文字系统直接实现Freetype也行,但是为了容易模块扩充,抽象出结构体,可以添加其他字体,如点阵或其他;

        3.4 UI系统

        所谓UI,就是UserInterface(用户界面),有图像界面(GUI)等;我们的UI系统,就是构造各类GUI元素,比如按钮(目前只实现按钮)

3.4.1 抽象数据结构

        抽象一个结构体描述一个按钮;

typedef struct Button {
	int iFontSize;
	char *name;//ui界面按钮名字
	int status;//状态标记位
	Region tRegion;//ui界面按钮形状尺寸
	ONDRAW_FUNC OnDraw ;//绘制ptButton,保存到PDispBuff以便刷到LED
	ONPRESSED_FUNC OnPressed ;//点击后反应
}Button,*PButton;

3.4.2 按钮

        在LCD上显示出一个按钮;实现绘制按钮区域,居中写文字;标记状态(按钮是否可触摸)触摸改变底色;

        设置按钮“name”,以及标志位;提供默认绘制和默认点击处理函数

/*初始化(PButton ptButton按钮,设置为
char *name ,PRegion ptRegion,ONDRAW_FUNC OnDraw ,
ONPRESSED_FUNC OnPressed参数*/
void Button_Init(PButton ptButton,char *name ,PRegion ptRegion,ONDRAW_FUNC OnDraw ,ONPRESSED_FUNC OnPressed)
{
	ptButton->status    =0;
	ptButton->name      = name;
	if(ptRegion)//函数填入有PRegion ptRegion才需要设置
		ptButton->tRegion   =*ptRegion;
	ptButton->OnDraw    =OnDraw ? OnDraw : DefaultOnDraw;
	ptButton->OnPressed =OnPressed ?OnPressed : DefaultOnPressed;
}

        默认函数解释:

DefaultOnDraw:默认绘制函数

  1. 在LCD上绘制底色方框;
  2. 在底色上填充文字,并设置文字大小;
		/*默认绘制按钮函数*/
static int DefaultOnDraw(struct Button *ptButton,PDispBuff ptDispBuff)
{
				/*绘制底色*/
	DrawRegion(&ptButton->tRegion,BUTTON_DEFAULT_COLOR);

				/*居中写文字*/
	SetFontSize(ptButton->iFontSize);
	DrawTextInRegionCentral(ptButton->name,&ptButton->tRegion,BUTTON_TEXT_COLOR);

				/*flush to led/wed*/
	//FlushDisplayRegion(&ptButton->tRegion, ptDispBuff);
	return 0;
}

DefaultOnPressed:默认点击按钮函数

  1. 判断标记位是否改变;
  2. 若标记位变化,改变底色颜色绘制出
  3. 写文字
static int DefaultOnPressed(struct Button *ptButton,PDispBuff ptDispBuff,PInputEvent ptInputEvent)
{
	unsigned int dwColor =BUTTON_DEFAULT_COLOR;
	ptButton->status =!ptButton->status;
	if(ptButton->status)
		dwColor=BUTTON_PRESSED_COLOR;

				/*绘制底色*/
	DrawRegion(&ptButton->tRegion,dwColor);

				/*居中写文字*/
	DrawTextInRegionCentral(ptButton->name,&ptButton->tRegion,BUTTON_TEXT_COLOR);

				/*flush to led/wed*/
	FlushDisplayRegion(&ptButton->tRegion, ptDispBuff);
	return 0;
}

3.5 页面系统

        对于不同的产品,具有类似的界面,但是实现的功能不同,功能由业务系统实现;本项目重在学习,所以实现页面系统便于扩展实现不同产品 ,想实现不同产品只需要填充页面系统结构体  中的业务函数即可;

3.5.1  抽象页面系统结构体

typedef struct PageAction {
	char *name;
	void (*Run)(void *pParams);//页面函数
	struct PageAction *ptNext;
}PageAction,*PPageAction;

3.5.2 页面系统管理

        注册页面系统进链表,遍历取出需要的页面

static PPageAction g_ptPages = NULL;//链表头
void PageRegister(PPageAction ptPageAction)//将一个页面结构体注册进链表
{
	ptPageAction->ptNext=g_ptPages;
	g_ptPages=ptPageAction;
}
PPageAction Page(char *name)
{
	PPageAction ptTmp = g_ptPages;
	while(ptTmp)
	{
		if(strcmp(name, ptTmp->name) == 0)
			return ptTmp;
		ptTmp=ptTmp->ptNext;
	}
	return NULL;
}
void PagesRegister(void)//注册要使用的界面结构体
{
	extern void MainPageRegister(void);
	MainPageRegister();
}

3.6 业务系统 

        以上实现的五个系统只是框架,不具备功能,功能由业务系统提供;

对于本项目业务系统需实现:

  1. 处理配置文件
  2. 根据配置文件生成按钮、界面
  3. 处理输入事件
  4. 注册进页面系统
static void MainPageRun(void *pParams)
{
	InputEvent tInputEvent;
	PButton ptButton;
	PDispBuff ptDispBuff;
	ptDispBuff= GetDisplayBuffer();//获得LED分辨率
	int error;
		/*读取配置文件*/
	error=ParseConfigfile();
	if(error)
	{
		printf("ParseConfigfile err\n");
		return ;
	}
	/*根据配置文件生成按钮、界面*/
	GenerateButtons();
	while(1)
	{
		/*读取输入系统*/
		error=GetInputDeviceEvent(&tInputEvent);
			if(error)
			continue;//如果有错误重新执行、不退出
		/*根据输入系统找到按钮*/
		ptButton=GetButtonByInputEvent(&tInputEvent);
		if(!ptButton)
			continue;
		/*调用按钮的OnPressed函数*/
		ptButton->OnPressed(ptButton,ptDispBuff,&tInputEvent);
	}	
}
static PageAction g_tMainPage = {
	.name = "main",
	.Run  = MainPageRun,
};

void MainPageRegister(void)
{
	PageRegister(&g_tMainPage);
}

3.6.1 处理配置文件

  配置文件:配置文件中有模块名称、是否可触摸、以及shell命令,

        处理配置文件为了使用配置文件中的信息绘制模块名称、判断是否可触摸以及实现命令脚本;

配置文件如下:

对于配置文件的每一行,都创建一个ItemCfg结构体

typedef struct Itemconfig {
	int index;//第几个按钮config
	char name[100];
	int CanbeTouched;
	char command[100];
}Itemconfig, *PItemconfig;

       以上内容均用链表管理结构体,该过程韦东山老师使用数组管理,为了代码的正确性,使用源码解释;

创建一个结构体数组;

static Itemconfig g_tItemconfigs[ITEMCONFIG_MAX_NUM];//用数组保存
static int g_iItemconfigCount =0;

解析配置文件 

  1. 打开配置文件
  2. 读取每一行内容
  3. 取出有用信息保存在数组中
  4. 提供其他函数便于管理
/*解析配置文件*/
int ParseConfigfile(void)
{	
	FILE *fp;//文件指针,fopen函数返回值
	char buf[100];
	char *p=buf;
	/*1. open config file */
	fp=fopen(CFG_FILE,"r");//第一参数是文件路径(字符串),第二模式
	if(!fp)
	{
		printf("can not open config file %s \n",CFG_FILE);
		return -1;
	}
		/*2.1 read each line*/
	while(fgets(buf,100, fp))
	{
		//buf[99]='\0'//fgets结尾会自动加,不会溢出
		/*2.2 忽略注释*/
		p=buf;
		if(*p=='#')
			continue;	
		/*2.3 吃掉空格或者TAB键*/
		while(*p==' '||*p=='\t')
			p++;
		/*2.4 处理*/
		g_tItemconfigs[g_iItemconfigCount].index=g_iItemconfigCount;
		g_tItemconfigs[g_iItemconfigCount].command[0]='\0';
		sscanf(p,"%s %d %s",g_tItemconfigs[g_iItemconfigCount].name,&g_tItemconfigs[g_iItemconfigCount].CanbeTouched,g_tItemconfigs[g_iItemconfigCount].command);
		g_iItemconfigCount++;		

	}
	return 0;
}
	/*计算有多少个按钮*/
int GetItemconfigCount (void)
{
	return g_iItemconfigCount;
}

	/*通过index找到Itemconfig结构体*/
PItemconfig GetItemconfigByIndex (int index)
{
	if(index< g_iItemconfigCount)
		return &g_tItemconfigs[index];//返回第index项Itemconfig结构体;
	else 
		return NULL;
}
	
	/*通过name找到Itemconfig结构体*/
PItemconfig GetItemconfigByName (char *name)
{
	int i;
	for(i=0;i<g_iItemconfigCount;i++)
	{
		if(strcmp(g_tItemconfigs[i].name,name)==0)
			return &g_tItemconfigs[i];
	}
	return NULL;
}

3.6.2 生成按钮

        处理配置文件后,解析的数据保存在结构体中,可根据结构体中数据生成按钮;

为了美观以及匹配LCD显示屏,详细计算按钮底色大小、高宽比、文字外框

  1. 获得LCD分辨率
  2. 计算出按钮单个尺寸
  3. 居中显示:计算每个按钮的region
  4. 得到按钮外框调用Button_Init
  5. 设置字体大小,根据解析配置文件中的name计算字符串外框,并缩放成按钮底色外框;
  6. 逐一绘制

本过程计算偏多,只解释重要源码;

        计算出按钮底色大小后,将数据保存在结构体中,调用页面系统中的按钮函数,提供业务按钮点击函数;循环绘制每一个按钮;


for(row=0;(row < rows) && (i < n);row++)
	{
		pre_start_y = start_y + row * Height;
		pre_start_x = start_x - Width;
		for(column=0;(column < n_per_line) && (i < n);column++)
		{
			ptButton= &g_tButtons[i];   
			ptButton->tRegion.iHeigh= Height - GAP;
			ptButton->tRegion.iWidth=Width - GAP;
			ptButton->tRegion.iLeftUpX=pre_start_x + Width;
			ptButton->tRegion.iLeftUpY=pre_start_y;
			pre_start_x=ptButton->tRegion.iLeftUpX;
				/*Button_Init*/
			Button_Init(ptButton, GetItemconfigByIndex(i)->name,  NULL,NULL , MainPageOnPressed);
			i++;
		}
	}
	/*OnDraw*/
	for (i = 0; i < n; i++)
	{
		g_tButtons[i].iFontSize = iFontSize;
		g_tButtons[i].OnDraw(&g_tButtons[i], ptDispBuff);
	}

        业务按钮点击函数:根据解析后的配置文件,判断是否点击,可点击触摸屏输入,点击后改变底色颜色,对于网络事件,接受到网络信息判断是否字符串中是否有“ok”,有则改变底色颜色代表测试成功;

static int MainPageOnPressed(struct Button *ptButton,PDispBuff ptDispBuff,PInputEvent ptInputEvent)
{
	char name[100] ;
	char status[100];
	char *strButton;
	strButton=ptButton->name;
	unsigned int dwColor =BUTTON_DEFAULT_COLOR;

		/*1. 对于触摸屏事件*/
	if(ptInputEvent->iType==INPUT_TYPE_TOUCH)
	{	
		/*1.1 分辨能否被点击*/
		if(GetItemconfigByName(ptButton->name)->CanbeTouched==0)
			return -1;
		/*1.2 修改颜色*/
		if(ptInputEvent->iPressure==0)
		{
			ptButton->status =!ptButton->status;
			if(ptButton->status)
			dwColor=BUTTON_PRESSED_COLOR;
		}
	}
	
		/*2 网络事件*/
	else if(ptInputEvent->iType==INPUT_TYPE_NET)
	{
		/*2.1  根据传入字符串修改颜色:wifi ok, wifi err,burn 70*/

		sscanf(ptInputEvent->str,"%s %s",name,status);//从ptInputEvent->str提取保存在name中
		if(strcmp(status,"ok")==0)
			dwColor=BUTTON_PRESSED_COLOR;
		else if(strcmp(status,"err")==0)
			dwColor =BUTTON_DEFAULT_COLOR;
		else if(status[0]>='0'&&status[0]<='9')
		{
			dwColor =BUTTON_PERCENT_COLOR;
			strButton=status;
		}
		else 
			return -1;
	}
	else
	{
		return -1;
	}

	
	/*绘制底色*/
	DrawRegion(&ptButton->tRegion,dwColor);

				/*居中写文字*/
	DrawTextInRegionCentral(strButton,&ptButton->tRegion,BUTTON_TEXT_COLOR);

				/*flush to led/wed*/
	FlushDisplayRegion(&ptButton->tRegion, ptDispBuff);
	return 0;
}

3.6.3 输入事件处理

读取输入事件,根据输入事件找到对应按钮;调用按钮点击函数

	while(1)
	{
		/*读取输入系统*/
		error=GetInputDeviceEvent(&tInputEvent);
			if(error)
			continue;//如果有错误重新执行、不退出
		/*根据输入系统找到按钮*/
		ptButton=GetButtonByInputEvent(&tInputEvent);
		if(!ptButton)
			continue;
		/*调用按钮的OnPressed函数*/
		ptButton->OnPressed(ptButton,ptDispBuff,&tInputEvent);
	}

3.7 综合应用

初始化每个系统,直接运行业务函数

int main(int argc, char **argv)
{
	int error;
	if (argc != 2)
	{
		printf("Usage: %s <font_file> \n", argv[0]);
		return -1;
	}	
		/*初始化显示系统*/
	DisplayInit();
	SelectDefaultDisplay("fd");
	InitDefaultDisplay();
		/*初始化输入系统*/
	InputInit();
	InputDeviceInit();
		/*初始化文字系统*/
	FontRegister();
	error = SelectAndInitFont("freetype", argv[1]);
	if (error)
	{
		printf("SelectAndInitFont err\n");
		return -1;
	}
		/*初始化页面系统*/
	PagesRegister();
		/*运行业务系统的主页面*/
	Page("main")->Run(NULL);
		
	return 0;
}

4. 总结

           本项目重点在于学习如何开发一个产品不仅仅局限于电子产品量产工具;项目中的shell脚本编写、makefile管理本文不做赘述;

        项目提供详细的模版,程序分层明确,内容涉及framebuffer、socket、freetype 库,tslib 库、多线程编程、同步互斥机制等知识点;

以上内容参考韦东山老师项目,建议观看原讲解。

以上内容为自己总结,难以避免出现错误,仅供参考。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值