嵌入式Linux项目-电子产品量产工具
本项目由韦东山老师担任

百问网的韦东山老师是嵌入式领域专家, 他致力于课程开发与硬件研发
GIT下载代码,如下所示:
$ git clone https://e.coding.net/weidongshan/01_all_series_quickstart.git
bash
1. 项目简介
本项目有如下特点:
将这套软件复制到SD卡上后插入IMX6ULL开发板并启动系统后会立即开始执行各组件的测试以及EMMC系统的烧录流程操作人员只需按照指导说明书连接几个关键模块即可完成整个测试流程以及烧录操作一旦显示完整且所有功能模块标识灯变为绿色则表示测试成功完成
该软件支持灵活配置和无限制扩展功能。 通过配置文件可增添任意数量的测项目标,并根据需要为每个测项目标独立编排相应的测验任务流程,在全部测验任务完成之后将结果直接输出至人机交互界面(GUI)中展示即可。 各个测验任务流程之间相互不影响
③ 纯C语言编程

2. 项目框架
将该项目划分为若干个子系统;采用面向对象编程与模块化编程的方法,在设计时会通过抽取关键功能并将其转化为独立的结构体来实现系统的扩展性管理。每个这样的结构体会包含一个指针域以指向其后续的结构体以简化数据管理和操作流程;该项目主要包括以下几个组成部分:显示系统、输入系统等

显示系统:以Framebuffer为基础,在启动LCD设备节点后进行操作以采集分辨率及相关参数信息,并对Framebuffer进行映射处理从而完成描点功能的实现;
输入系统:基于TS-lib库与UDP协议的数据接收系统;该系统由触控屏接口与网络通信模块组成;通过环形缓冲区实现数据的存储与管理;并设计多线程机制以支持多种设备的数据接收请求
文字系统:基于Freetype库开发,实现矢量字符串,表示检测模块名称;
UI系统:在LCD上绘制各模块按钮
业务系统:调用各模块接口;处理配置文件、生成界面、处理输入事件等
页面系统:实现页面注册、页面管理
3. 源码分析
3.1 显示系统

- 构建显示系统的数据模型,并遵循模块化设计原则为每个功能创建相应的数据对象。
- 将基础功能单元映射到统一的数据管理架构中,并支持构建多个展示子系统。
- 将基础数据对象链接至统一的数据管理架构,并对整个数据集合进行一次完整的访问以提取并定义展示系统的公共接口函数库。
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;
cpp

typedef在这里做了两件事:
它将DispOpr指定为struct DispOpr的别名。这表明,在编程过程中,允许你使用DispOpr来声明这种类型的变量,并非每次都必须写出完整的结构名称.`
此外还为P\text{Dis}p\text{Op}r命名为指向\text{Dis}p\text{Op}r类型变量的指针。这样一来,在声明指向该类型变量的指针时,则允许你在代码中直接使用该名称,并无需写出完整的结构体名称\text{struct }\text{Dis}p\text{Op}r*。
3.1.2 Framebuffer
为了在Linux系统中控制LCD屏,请使用Framebuffer驱动程序。其中,“Frame”意指帧,“Buffer”意指缓冲。这意味著Framebuffer实际上相当于一块内存。在FBMEA架构中设置了多个独立框线,在这些框线之间传递信号和数据以实现人机交互功能。FBMEA部分中的每个框线都是一个独立的框线。假设LCD屏幕分辨率设定为1024x768像素,则其占用的空间计算如下:每个像素的颜色信息占用32位,则 framebuffer 的大小就是:1024×768×32/8= 3,145,728 字节
改写说明
简单介绍LCD的操作原理:
- 控制程序配置LCD控制器: 首先依据LCD参数配置其时序与极性; 然后基于分辨率与位宽分配Frame Buffer。
- 应用通过ioctl指令获取LCD分辨率及位宽信息
- 应借助mmap函数将数据映射至Frame Buffer中

本项目表明显示系统借助FrameBuffer(帧缓存)来实现其功能。FrameBuffer作为底层功能模块仅需填充相应的显示系统数据结构体即可完成任务,并由上层程序负责管理这些数据处理过程。
static DispOpr g_tFramebufferopr = {
.name = "fd",
.GetBuffer =FdGetbuffer,//用于获得buffer设定图案形状
.FlushRegion=FDFlushRegion,//将buffer刷到屏幕
.Device_init=FDdevice_init,
.Device_exit=FDdevice_exit,
};
cpp
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;
}
cpp

FDdevice_exit():取消映射、关闭设备节点;
static int FDdevice_exit(void)
{
munmap(fb_base, screen_size);
close(fd_fb);
return 0;
}
cpp
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;
}
cpp
FDFlushRegion():空!本项目通过描点函数绘制图案
将g_tFramebufferopr结构体注册进链表
void RegisterFramebuffer(void)
{
RegisterDisplay (&g_tFramebufferopr);
}
cpp
3.1.3 显示系统管理器
承上启下、注册底层模块结构体、遍历链表、提供API接口
- 注册为显示设备结构体,并以链表形式构造(采用头插法)。
- 将framebuffer加入到链表中。
- 根据指定的' name '变量选择默认显示器。
- 对选定的默认显示器进行初始化设置。
- 获取当前显示设备的buffer内容。
- 进行区域更新 --- 使用描点算法完成显示效果。
注册显示系统结构体,链表(头插法)
void RegisterDisplay (PDispOpr PtDispOpr)// 注册结构体
{
/* 头插法 头结点g_DispDevs为一个指针,指向下一个节点首地址*/
PtDispOpr->ptNext = g_DispDevs;
g_DispDevs = PtDispOpr;
}
cpp
将framebuffer注册进链表
void Registerdevice(void)
{
extern void RegisterFramebuffer(void);
RegisterFramebuffer();
}
cpp
根据“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;
}
cpp

初始化默认显示器:调用底层指针函数,并获得显示系统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;
}
cpp

显示器刷区域函数为空便于扩展所以写出
本项目使用描点函数实现显示
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;
}
}
cpp

接收的dwcolor用于表示颜色信息其标准格式为十六进制数前缀0x00后接六位RGB分量当显示分辨率设置为16位像素深度时需从该数值中提取红绿蓝三个分量然后按照RGB565编码模式分别保留红绿蓝通道的有效比特数最后生成一个新的十六位代码块并将其写入Framebuffer缓冲区对于32bit pixels深度则可直接将数值输出到Framebuffer缓冲区
计算给定(x y)坐标位置对应于 framebuffer 的物理存储地址
对于8bit pixels深度需将该数值解码以获得对应的rgb三基色分量这一过程涉及调色表映射机制其中该数值被视为特定调色表中的索引值
3.2 输入系统
在本项目中输入设备有两个:触摸屏输入、网络输入。
若led灯需操作人员观察是否闪烁,则按下led按钮以使其变为绿色以表示测试成功;
对于其他设备如ap3216c, 经脚本自检无异常, 它会发送至指定ip地址的正常通知, 当经网络收到该通知时, 相应按钮将变绿以表示测试成功。
输入系统框架:
- 构建抽象输入系统的数据结构体,并采用模块化设计方法对单个功能进行相应的数据结构体构造。
- 底层功能模块负责填充数据结构体的内容,并完成触摸屏输入与网络输入的处理。
- 将底层数据结构体注册到链表中,并通过遍历链表的方式抽象出完整的输入系统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;
cpp

3.2.2 触摸屏输入
触摸屏输入事件利用tslib库实现;tslib 是一个开源项目;该库可被用来访问触摸屏设备
应用tslib库非常便捷;在对开发板进行tslib库的交叉编译完成后,通过调用几个特定的接口函数即可轻松获取触摸屏的数据信息;
- 1、打开设备节点
- 2、读取触摸屏事件
- 3、关闭设备节点
实现基于触摸屏输入事件的结构体、并注册进链表
static InputDevice g_tTouchscreenDev={
.name ="touchscreen",
.GetInputEvent =TouchscreenGetInputEvent,
.DeviceInit =TouchscreenDeviceInit,
.DeviceExit =TouchscreenDeviceExit,
};
void TouchscreenRegister(void)
{
RegisterInputDevice(&g_tTouchscreenDev);
}
cpp

TouchscreenDeviceInit:
ts_setup函数将执行以下操作:首先打开设备节点并创建一个TSDEV结构体;随后将执行以下操作:读取配置文件并进行处理。
static int TouchscreenDeviceInit (void)
{
/*打开配置设备,文件名;NULL,0:非阻塞*/
g_ts = ts_setup(NULL, 0);
if (!g_ts)
{
printf("ts_setup err\n");
return -1;
}
return 0;
}
cpp

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;
}
cpp

关闭设备节点
static int TouchscreenDeviceExit(void)
{
ts_close(g_ts);
return 0;
}
cpp
3.2.3 网络输入
该网络输入采用UDP协议作为基础架构,在程序运行过程中模拟的是一个服务器角色,并通过socket标准接口实现对客户端发送消息的接收与处理机制
实现基于网络输入的输入事件结构体、并注册进链表
static InputDevice g_tNetinputDev={
.name ="net",
.GetInputEvent = NetGetInputEvent,
.DeviceInit = NetDeviceInit,
.DeviceExit = NetDeviceExit,
};
void NetInputRegister(void)
{
RegisterInputDevice(&g_tNetinputDev);
}
cpp

函数解析;
static int NetDeviceInit (void):
- 建立并配置一个套接字用于设置套接字的创建
- 将地址与该套接字关联
- 避免基于连接的通信以采用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;
}
cpp

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;
}
cpp

关闭socket接口
static int NetDeviceExit(void)
{
close(server_socket_fd);
return 0;
}
cpp
3.2.4 环形缓冲区
触摸屏输入可能一次性提交多组数据, 网络端点可能会从多个客户端接收到数据, 不应仅依赖单一变量来存储数据, 因此建议采用数组形式, 并结合环形缓冲区机制
定义写指针、读指针、缓存区
#define BUFFER_LEN 20
static int g_iRead;
static int g_iWrite;
static InputEvent g_atInputEvents[BUFFER_LEN];//缓存区
cpp
环形缓冲区中最重要两个状态:缓冲区满、缓冲区空
缓冲区满:写指针的下一个位置等于读指针的位置。
缓冲区空:读指针等于写指针的位置。
static int isInputBufferFull(void)
{
return (g_iRead == ((g_iWrite + 1) % BUFFER_LEN));
}
static int isInputBufferEmpty(void)
{
return (g_iRead == g_iWrite);
}
cpp
当缓冲区非满时可以写缓冲区,将输入事件结构体中的值写入缓冲区
当缓冲区非空时可以读缓冲区,将缓冲区中读位置值读取出
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;
}
}
cpp

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();
}
cpp

初始化所有输入设备,获得输入数据;
通过遍历链表的方式获取输入设备。
因为采用轮询方式可能导致数据丢失。
为了防止多个线程对环形缓冲区同时操作而引发竞争问题。
当主程序读取空的环形缓冲区时,则需等待子程序完成写入操作。
通过在每个输入设备上启动独立线程来确保数据完整性,并将事件数据按顺序存入环形缓冲区。
为了避免资源冲突问题,在所有子线程访问该区域前必须先申请互斥锁。
代码如下:
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;
}
cpp

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;
}
cpp

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;
}
cpp

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;
cpp

3.3.2 Freetype
Freetype 是一个开源的字体引擎库,在获取多种字体格式文件的同时提供了一致化的接口来生成矢量字体图形。我们可以通过迁移这个字体引擎并调用相应的API接口来完成指定功能:将提供的字体文件导入系统后即可自动生成关键点坐标、描绘闭合曲线并填充所需颜色以实现矢量图形显示效果。
freetype 库使用步骤
1. 初始化:FT_InitFreetype
FT_Library library;
FT_Init_FreeType(&library);
// 初始化 freetype 库,并创建一个 FT_Library 类型的对象,用于存储库的实例信息。
cpp
2. 加载(打开)字体Face:FT_New_Face
FT_Face face;
FT_New_Face(library, "path/to/font.ttf", 0, &face);
//用于从字体文件中加载一个字体面(Face)
cpp
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字符编码。
cpp
根据编码值charcode确定glyph_index:GlyphIndex = FT_Get_Char_Index(face, charcode)
FT_UInt glyph_index = FT_Get_Char_Index(face, charcode);
//函数根据给定的字符编码值(如Unicode值)返回对应的字形索引
cpp
6. 根据glyph_index取出glyph:FT_Load_Glyph(face,glyph_index)
FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT);
//函数加载指定字形索引的字形数据(如轮廓、度量信息等)到内存中,准备进行渲染。
cpp
7. 转为位图:FT_Render_Glyph
FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);
//函数将加载的字形数据转换为位图格式
cpp
8. 移动或旋转:FT_Set_Transform
FT_Matrix matrix;
FT_Vector pen;
FT_Set_Transform(face, &matrix, &pen);
//函数用于设置字体的变换矩阵和偏移量,以实现移动或旋转效果
cpp
9. 最后显示出来。
在第5至第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);
}
cpp

源码解释:
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;
}
cpp

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;
}
cpp

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;
}
cpp

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);
}
cpp

本项目的文字系统能够集成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;
cpp
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;
}
cpp

默认函数解释:
DefaultOnDraw:默认绘制函数
- 在LCD上绘制底色方框;
- 在底色上填充文字,并设置文字大小;
/*默认绘制按钮函数*/
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;
}
cpp

DefaultOnPressed:默认点击按钮函数
- 判断标记位是否改变;
- 若标记位变化,改变底色颜色绘制出
- 写文字
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;
}
cpp

3.5 页面系统
对于不同的产品而言,在界面设计上相似性较高,并且功能模块各有特色;本项目以学习为主,在此背景下便于开发人员后续开发和扩展相应的产品功能;具体而言,在现有架构下只需在页面系统结构体内填充相应的业务模块即可。
3.5.1 抽象页面系统结构体
typedef struct PageAction {
char *name;
void (*Run)(void *pParams);//页面函数
struct PageAction *ptNext;
}PageAction,*PPageAction;
cpp
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();
}
cpp

3.6 业务系统
以上实现的五个系统只是框架,不具备功能,功能由业务系统提供;
对于本项目业务系统需实现:
- 管理配置文件
- 根据配置文件生成按钮和界面
- 指挥并处理用户输入事件
- 登录到页面系统
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);
}
cpp

3.6.1 处理配置文件
配置文件:配置文件中有模块名称、是否可触摸、以及shell命令,
解析配置文件信息以支持生成模块名称、确定设备是否可探测以及创建执行命令的脚本
配置文件如下:

对于配置文件的每一行,都创建一个ItemCfg结构体
typedef struct Itemconfig {
int index;//第几个按钮config
char name[100];
int CanbeTouched;
char command[100];
}Itemconfig, *PItemconfig;
cpp
在处理上述内容时,在开发团队中采用链表结构体来组织数据。具体而言,在该过程中教师采用数组管理逻辑,并通过分析源代码来理解其运行机制。
创建一个结构体数组;
static Itemconfig g_tItemconfigs[ITEMCONFIG_MAX_NUM];//用数组保存
static int g_iItemconfigCount =0;
cpp
解析配置文件
- 访问配置文件设置以获取所需参数。
- 解析系统日志中的每一行记录。
- 提取关键数据并存储于列表中。
- 实现辅助功能以提高系统的可扩展性。
/*解析配置文件*/
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;
}
cpp

3.6.2 生成按钮
完成对配置文件的处理后
为了美观以及匹配LCD显示屏,详细计算按钮底色大小、高宽比、文字外框
1 获取LCD分辨率值
2 确定各个按钮的尺寸参数
3 对各按钮进行居中显示处理
4 在确定了按钮外框后调用Button.Init函数
5 根据配置文件设置字体大小,并基于名称参数生成字符串对应的区域;然后将该区域按照字符宽度缩放以匹配对应的底色范围
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);
}
cpp

业务按钮点击函数:根据解析后的配置文件进行设置,在必要时执行触摸屏输入操作;当完成一次点击动作后会改变底色颜色;对于接收到的网络数据,在分析其中的内容时会检查字符串中是否存在"ok"标识符;如果检测到"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;
}
cpp

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

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;
}
cpp

4. 总结
本项目的核心目标是掌握开发产品的相关方法,并不在仅限于专注于电子产品中的量产工具这一领域。其中关于Shell脚本编写以及Makefile的管理方面,在本次讨论中不予详细阐述。
项目提供了丰富的设计模板,并且层次分明地组织了各个功能模块;所涉及的知识点包括帧缓冲机制(framebuffer)、socket通信库(socket)、自由类型库(freetype)以及tslib库的支持;同时深入探讨了多线程编程技术(multi-thread programming)和同步与互斥机制(synchronization and mutual exclusion)等关键概念。
以上内容参考韦东山老师项目,建议观看原讲解。
以上内容为自己总结,难以避免出现错误,仅供参考。
