Linux应用项目之量产工具(一)——显示系统
目录
前言
项目特点及介绍
① 简单易用
② 软件可配置、易扩展
③ 纯 C 语言编程
软件总框架
显示系统
1.数据结构抽象
disp_manager.h
2.Framebuffer编程
framebuffer.c
3.显示管理
disp_manager.c
4.单元测试
disp_test.c
顶层目录Makefile
顶层目录Makefile.build
底层目录display中的Makefile
底层目录unittest中的Makefile
前言
今天开始学习Linux的第一个实战项目——电子产品量产测试与烧写工具,简称量产工具。这是一套软件,用在我们的实际生产中,有如下特点:
项目特点及介绍
① 简单易用
把这套软件烧写在 SD 卡上,插到 IMX6ULL 板子里并启动,它就会自动测试各个模块、烧写 EMMC 系统。
工人只要按照说明接入几个模块,就可以完成整个测试、烧写过程。
测试结果一目了然:等 LCD 上所有模块的图标都变绿时,就表示测试通过。
② 软件可配置、易扩展
通过配置文件添加测试项,可以添加不限个数的测试项。
每个测试项有自己的测试程序,测试通过后把结果发送给 GUI 即可。各个测试程序互不影响。
③ 纯 C 语言编程
下图是这个工具的界面,它可以一边测试一边烧写:
上图中的 led、speaker 按钮,可以点击:
① 当你看到 LED 闪烁时,就点击 led 按钮,它变成绿色表示测试通过;
② 当你从耳机里听到声音时,就点击 speaker 按钮,它变成绿色表示测试通过。
其他按钮无法点击,接上对应模块后会自动测试,测试通过时图标就会变绿。
上图中的蓝色按钮表示烧写 EMMC 的进度,烧写成功后它也会变绿。
LCD 上所有图标都变绿时,就表示测试、烧写全部完成;某项保持红色的话,就表示对应模块测试失败。
对这个项目,我们要拆分出多个子系统,并且这些子系统与业务无关,以后还可以应用在其他项目上;我们编写各个子系统的代码时,要抽象出它的对外接口,增加它的扩展性,减少和其他模块的耦合性。通过这个项目,能锻炼我们面向对象的编程思想以及对事物的抽象能力等等。这个项目可以无限扩展,比如:
软件总框架
要想最大程度复用我们写的代码,就需要模块化编程,拿显示系统举例,我们可以通过Framebuffer把数据显示到屏幕上,我们就可以构造出一个结构体,调用结构体里的初始化函数等等来使用这个模块;对于有些设备,它可能不需要用到屏幕,而是用到web输出,通过网络的方式显示数据,在网页打印出来,假设我们以后对于显示系统还要添加更多的模块,对于每个模块我们都要构造出它的结构体吗?显然是不用的,我们可以抽象出同一个结构体类型。
显示系统
1.数据结构抽象
来看看这个框图,我们要先获得一个buffer,我们可以自己在这个buffer里绘制我们想要的图案,绘制好后可以通过Framebuffer刷到屏幕上显示,也可以通过网络传输在浏览器上显示出来。
我们可以定义出下面这个结构体:
disp_manager.h
#ifndef _DISP_MANAGER_H
#define _DISP_MANAGER_H/*编译的时候报错说NULL未定义,我们干脆自己定义好了*/
#ifndef NULL
#define NULL (void *)0
#endif/*存放buffer的信息*/
typedef struct DispBuff {int iXres;/*x坐标的像素值*/int iYres;/*y坐标的像素值*/int iBpp;/*每个像素有多少位(像素的大小)*/char *buff;/*指向得到的buffer,然后就可以操作它了*/
}DispBuff, *PDispBuff;/*存放图像的区域信息*/
typedef struct Region {int iLeftUpX;/*区域左上角的x坐标*/int iLeftUpY;/*区域左上角的y坐标*/int iWidth;/*宽*/int iHeight;/*高*/
}Region, *PRegion;/*模块调用这个结构体里的函数,需要我们自己去实现里面的函数*/
typedef struct DispOpr {char *name;int (*DeviceInit)(void);//初始化函数int (*DeviceExit)(void);//退出函数int (*GetBuffer)(PDispBuff ptDispBuff);//获得buffer,以便绘制图像int (*FlushRegion)(PRegion ptRegion, PDispBuff ptDispBuff);//将buffer刷到对应的设备
(屏幕/网页),将ptDispBuff结构体指针指向的结构体里的buffer信息刷到Region结构体里(存放区域的信息)
表示你想在哪里显示?struct DispOpr *ptNext;//用链表管理这些模块
}DispOpr, *PDispOpr;/*****************************************/
/** 这些声明的函数我们后面再写,我先给出源码,先忽略这部分*/
void RegisterDisplay(PDispOpr ptDispOpr);void DisplayInit(void);
int SelectDefaultDisplay(char *name);
int InitDefaultDisplay(void);
int PutPixel(int x, int y, unsigned int dwColor);
int FlushDisplayRegion(PRegion ptRegion, PDispBuff ptDispBuff);
PDispBuff GetDisplayBuffer(void);#endif
这些结构体都是一步一步改善过来的,所以我尽量把注释写的详细一点,我们想要在屏幕或者网页(网页暂时没有实现)上显示,我们首先要实现 struct DispOpr 这个结构体里的各个函数,然后定义出各个模块的结构体,就可以调用定义出来的结构体里的函数实现各个功能。
2.Framebuffer编程
对于Framebuffer编程不熟悉的同学可以去看我这篇博客Framebuffer应用编程,下面用到的函数都是直接复制之前写的代码,原理就不再讲解了。
framebuffer.c
我们先定义出给LCD屏幕使用的结构体,再去实现底层的函数。以后上层APP就可以调用结构体里的 DeviceInit 等函数,不需要关心底层的驱动代码。这就是面向对象的编程思想。
源码如下:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <disp_manager.h>static int fd_fb;
static struct fb_var_screeninfo var; /* Current var */
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_width;
static unsigned int pixel_width;/*初始化函数*/
static int FbDeviceInit(void)
{/*打开设备结点,获得LCD参数等等*/fd_fb = open("/dev/fb0", O_RDWR);if (fd_fb < 0){printf("can't open /dev/fb0\n");return -1;}if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var)){printf("can't get var\n");return -1;}/*使用mmap函数获得可以直接操作LCD的buffer*/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;fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);if (fb_base == (unsigned char *)-1){printf("can't mmap\n");return -1;}return 0;
}/*退出函数*/
static int FbDeviceExit(void)
{/*释放buffer,关闭设备结点*/munmap(fb_base, screen_size);close(fd_fb);return 0;
}/* 可以返回LCD的framebuffer, 以后上层APP可以直接操作LCD, 可以不用FbFlushRegion* 也可以malloc返回一块无关的buffer, 要使用FbFlushRegion*/
static int FbGetBuffer(PDispBuff ptDispBuff)
{ptDispBuff->iXres = var.xres;ptDispBuff->iYres = var.yres;ptDispBuff->iBpp = var.bits_per_pixel;ptDispBuff->buff = (char *)fb_base;return 0;
}/*为了方便,我们直接得到可以操作LCD的buffer,因此不需要Flush函数*/
static int FbFlushRegion(PRegion ptRegion, PDispBuff ptDispBuff)
{return 0;
}static DispOpr g_tFramebufferOpr = {.name = "fb",.DeviceInit = FbDeviceInit,.DeviceExit = FbDeviceExit,.GetBuffer = FbGetBuffer,.FlushRegion = FbFlushRegion,
};/*RegisterDisplay函数是将显示系统的所有设备注册到链表里统一管理,这在后面会讲,这里先忽略*/
void FramebufferInit(void)
{RegisterDisplay(&g_tFramebufferOpr);
}
这里就是实现了底层的驱动函数,实现 DispOpr 结构里的函数。
3.显示管理
我们的应用程序当然可以直接使用我们上面构造的结构体,但是我有多个显示模块,我们还需要一个公共的部分,来选择我们要使用的模块,所以需要还需要写出管理底层模块的代码。
框图如下:
我们要写出 disp_manager.c 这个程序,它起到承上启下的作用,很多函数其实也只是调用最底层的代码,但是我们再经过一层封装,就非常方便的实现管理底层的模块
disp_manager.c
#include <stdio.h>
#include <string.h>
#include <disp_manager.h>/* 管理底层的LCD、WEB */
static PDispOpr g_DispDevs = NULL;/*创建一个空的链表*/
static PDispOpr g_DispDefault = NULL;/*这个指针指向想要操作的模块,相当于句柄*/
static DispBuff g_tDispBuff;
static int line_width;
static int pixel_width;/*描点函数,是所有图像的基础,我之前写的Framebuffer应用编程里有讲*/
int PutPixel(int x, int y, unsigned int dwColor)
{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);return -1;break;}}return 0;
}/*注册函数,把模块放进链表里统一管理*/
void RegisterDisplay(PDispOpr ptDispOpr)
{ptDispOpr->ptNext = g_DispDevs;g_DispDevs = ptDispOpr;
}/* 选择函数,传入想要操作的底层模块的名字,如"fb",表示使用Framebuffer在屏幕上显示* 遍历链表,如果模块注册进链表里并且找到了名字,g_DispDefault就会指向我们想操作的模块的结构体*/
int SelectDefaultDisplay(char *name)
{PDispOpr pTmp = g_DispDevs;while (pTmp) {if (strcmp(name, pTmp->name) == 0){g_DispDefault = pTmp;return 0;}pTmp = pTmp->ptNext;}return -1;
}/*初始化函数,其实就是调用底层的初始化*/
int InitDefaultDisplay(void)
{int ret;/*调用g_DispDefault指向的结构体,调用底层的模块初始化函数*/ret = g_DispDefault->DeviceInit();if (ret){printf("DeviceInit err\n");return -1;}/*获得buffer,参数会保存在传入的结构体里面*/ret = g_DispDefault->GetBuffer(&g_tDispBuff);if (ret){printf("GetBuffer err\n");return -1;}/*这两个参数是为了给描点函数用的,看描点函数第一行计算pen_8的地址,需要这两个参数*/line_width = g_tDispBuff.iXres * g_tDispBuff.iBpp/8;pixel_width = g_tDispBuff.iBpp/8;return 0;
}/*把获得到的buffer返回给上层代码*/
PDispBuff GetDisplayBuffer(void)
{return &g_tDispBuff;
}/*Flush函数,底层的Flush函数我们并没有写,这里可以忽略*/
int FlushDisplayRegion(PRegion ptRegion, PDispBuff ptDispBuff)
{return g_DispDefault->FlushRegion(ptRegion, ptDispBuff);
}/*这里的初始化函数是将模块的结构体注册到链表里*/
void DisplayInit(void)
{extern void FramebufferInit(void);FramebufferInit();
}
虽然代码封装了很多层,但是这样便于以后添加更多的模块,更加容易扩展和移植。
4.单元测试
disp_test.c
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>#include <disp_manager.h>#define FONTDATAMAX 4096
/*略*/
需要自己定义点阵字体,这里我就省略了,可以看我关于Framebuffer的博客,或者去看韦东山老师的相关课程,里面有讲到哪里可以找到这个字体库。
/*********************************************************************** 函数名称: lcd_put_ascii* 功能描述: 在LCD指定位置上显示一个8*16的字符* 输入参数: x坐标,y坐标,ascii码* 输出参数: 无* 返 回 值: 无* 修改日期 版本号 修改人 修改内容* -----------------------------------------------* 2020/05/12 V1.0 zh(angenao) 创建***********************************************************************/
void lcd_put_ascii(int x, int y, unsigned char c)
{unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];int i, b;unsigned char byte;for (i = 0; i < 16; i++){byte = dots[i];for (b = 7; b >= 0; b--){if (byte & (1<<b)){/* show */PutPixel(x+7-b, y+i, 0xffffff); /* 白 */}else{/* hide */PutPixel(x+7-b, y+i, 0); /* 黑 */}}}
}int main(int argc, char **argv)
{Region region;PDispBuff ptBuffer;/*调用这个函数把所有模块注册到链表里(我们暂时只用到Framebuffer)*/DisplayInit();/*然后选择要操作的模块,传入名字*/SelectDefaultDisplay("fb");/*底层的初始化(打开设备节点等操作)*/InitDefaultDisplay();/*调用显示字符的函数,在这个函数里会调用的描点函数,所以我说描点函数是基础*/lcd_put_ascii(100, 100, 'A');/*这里是初始化一下要传入Flush函数的结构体*/region.iLeftUpX = 100;region.iLeftUpY = 100;region.iWidth = 8;region.iHeigh = 16;/*获得buffer*/ptBuffer = GetDisplayBuffer();/*刷到硬件上(暂时只有屏幕)*/FlushDisplayRegion(®ion, ptBuffer);return 0;
}
接下来给大伙看看函数的调用流程。
多层函数的封装看起来会很乱,但如果看过我之前一篇博客UART开发基础,这篇博客也是我第一次接触这种面向对象的编程思想,像我之前写代码,也是直接APP调用底层函数,快准狠,但是如果以后工作,这种方法显然不可行,而且也锻炼不了什么事物抽象的能力,面对一些大型的项目可能会无从下手,但是这些结构体里的参数,也不是一下子就能想出来的,都是边写边补,缺啥补啥。
上传到Ubuntu时,要整个文件上传,如下:
并且在要编译的文件夹下添加Makefile,可以看这篇博客Makefile保姆级教程
顶层目录Makefile
CROSS_COMPILE ?=
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
LDFLAGS :=
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += display/
obj-y += unittest/
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
顶层目录Makefile.build
PHONY := __build
__build:
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
include Makefile
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
__build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
底层目录display中的Makefile
EXTRA_CFLAGS :=
CFLAGS_file.o :=
obj-y += disp_manager.o
obj-y += framebuffer.o
底层目录unittest中的Makefile
EXTRA_CFLAGS :=
CFLAGS_file.o :=
obj-y += disp_test.o
量产工具的第一篇博客就到此为止,下一篇是量产工具的输入系统,希望大伙多多关注支持。