数码相册——电子书轮询方式支持多输入
- 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 参考资料:《嵌入式Linux应用开发手册》、《嵌入式Linux应用开发手册第2版》、《gcc中文手册》
- 开发环境:Linux 3.4.2内核、arm-linux-gcc 4.3.2工具链
- 源码仓库:https://gitee.com/d_1254436976/Embedded-Linux-Phase-3
目录
一、前言
在【1.7 数码相册—电子书(1)—实现】和【1.7 数码相册—电子书(2)—编写通用的Makefile】这两篇博文中,我们实现了在开发板中显示电子书,但是仍然存在一些缺点,这节课针对这些缺点来进行改进。
- 实现:
1、电子书支持多种输入方式,如标准串口输入实现翻页退出操作,触摸LCD实现翻页操作;
2、在main.c
中采用轮询的方式支持多种输入方式。
二、框架图
1、软件框架图
对于上述完成主要功能的5个部分:encoding编码部分、fonts获取字体点阵部分、display显示部分、input输入部分、draw协调部分
- encoding编码部分:去文件中获得编码信息,对于每个文件,保存的时候,系统对自动或手动的根据编码规范,对文件的信息进行编码:如保存为
ASCII、GBK、UTF-8、UTF16LE、UTF16BE
; - fonts获取字体点阵部分:根据获得的编码信息得到字体数据(LCD上表示为点阵);
- display显示部分:把字体数据(点阵)显示在LCD上;
- input输入部分:管理不同的输入方式,定制不同的输入方式所实现的控制,满足多种控制场合;
- draw协调部分 :组织各个模块部分进行合作,实现电子书的显示、翻页功能等。
2、Makefile框架图
由于整个分为如下3部分:顶层目录的Makefile、顶层目录的Makefile.build、各级子目录的Makefile。
所以对于新添加的input输入部分需要进行如下修改:其目录下也需要添加该目录下的Makefile,对于顶层目录的Makeifle
也需要添加obj-y += /input
,具体的在编译的时候会介绍。
而对于顶层的Makefile.build
,不需要进行修改,因为它是一个比较通用的文件,根据顶层目录的Makefile信息进入到各个子目录下进行预处理、编译、汇编,最后每个子目录与顶层得到的built-in.o
进行链接,最后得到可执行文件show_file
.
下面就来介绍添加的input输入部分。
三、input输入部分
1、管理者头文件input_manager.h
在这个文件:
- 定义一个结构体变量,拓展文件可以根据自己的情况:分配结构体、设置结构体、注册结构体;
- 定义一些函数,供外部文件调用。
#ifndef _INPUT_MANAGER_H
#define _INPUT_MANAGER_H
#include <sys/time.h>
#define INPUT_TYPE_STDIN 0
#define INPUT_TYPE_TOUCHSCREEN 1
#define INPUT_VALUE_UP 0
#define INPUT_VALUE_DOWN 1
#define INPUT_VALUE_EXIT 2
#define INPUT_VALUE_UNKONW -1
/* 输入事件信息结构体 */
typedef struct InputEvent {
struct timeval time;
int type;
int val;
}T_InputEvent, *PT_InputEvent;
/* 输入事件结构体 */
typedef struct InputOpr {
char *name;
int (*DeviceInit)(void);
int (*DeviceExit)(void);
int (*GetInputEvent)(PT_InputEvent ptInputEvent);
struct InputOpr *ptNext;
}T_InputOpr, *PT_InputOpr;
int RegisterInputOpr(PT_InputOpr ptFontOpr);
int InputInit(void);
void ShowInputOpr(void);
int GetInputEvent(PT_InputEvent ptInputEvent);
int AllInputDeviceInit(void);
int StdinInit(void);
int TouchScreenInit(void);
#endif /* _INPUT_MANAGER_H */
2、管理者实现文件input_manager.c
- 注册函数
每个输入方式的结构体,可以调用这个函数把结构体注册到链表中。
/* 函数名: 注册函数
* 函数功能:构建一个链表:把多个拓展文件的结构体“串”起来
* 函数实现:根据传入的结点,首先判断该链表头是否为空
* 空则,头结点指向传入的节点,且把节点的ptNext域指向NULL
* 不空则,尾插法插入链表
*/
int RegisterInputOpr(PT_InputOpr ptInputOpr)
{
PT_InputOpr ptTmp;
if (!s_ptInputOprHead) {
s_ptInputOprHead = ptInputOpr;
ptInputOpr->ptNext = NULL;
} else {
ptTmp = s_ptInputOprHead;
while (ptTmp->ptNext)
{
ptTmp = ptTmp->ptNext;
}
ptTmp->ptNext = ptInputOpr;
ptInputOpr->ptNext = NULL;
}
return 0;
}
- 显示函数
通过这个函数,可以把链表中所有input方式的名字打印出来,供用户查看所支持的input方式。
/* 显示支持拓展文件的名字 */
void ShowInputOpr(void)
{
int i = 0;
PT_InputOpr ptTmp = s_ptInputOprHead;
while (ptTmp) {
printf("%02d %s\n", i++, ptTmp->name);
ptTmp = ptTmp->ptNext;
}
}
- 初始化函数
调用此函数,可以把对应的input方式结构体放入链表中。
/* 初始化函数 */
int InputInit(void)
{
int error;
error = StdinInit();
error |= TouchScreenInit();
return error;
}
- 调用input设备事件函数
/* 调用所有的Input设备的事件发生函数,即轮询的方式获取LCD或标准输入的数据 */
int GetInputEvent(PT_InputEvent ptInputEvent)
{
/* 把链表中的InputOpr的GetInputEvent都调用一次,一旦有数据立即返回 */
PT_InputOpr ptTmp;
ptTmp = s_ptInputOprHead;
while (ptTmp) {
if (ptTmp->GetInputEvent(ptInputEvent) == 0)
return 0;
ptTmp = ptTmp->ptNext;
}
return -1;
}
- 设备初始化函数
初始化每个设备的函数(设置input设备、调整模式等)
/* 初始化所有支持的Input设备 */
int AllInputDeviceInit()
{
int error;
PT_InputOpr ptTmp;
error = -1;
ptTmp = s_ptInputOprHead;
while (ptTmp) {
if (ptTmp->DeviceInit() == 0)
error = 0;
ptTmp = ptTmp->ptNext;
}
return 0;
}
- 完整文件
#include <config.h>
#include <string.h>
#include <stdlib.h>
#include "input_manager.h"
static PT_InputOpr s_ptInputOprHead; //链表头
/* 函数名: 注册函数
* 函数功能:构建一个链表:把多个拓展文件的结构体“串”起来
* 函数实现:根据传入的结点,首先判断该链表头是否为空
* 空则,头结点指向传入的节点,且把节点的ptNext域指向NULL
* 不空则,尾插法插入链表
*/
int RegisterInputOpr(PT_InputOpr ptInputOpr)
{
PT_InputOpr ptTmp;
if (!s_ptInputOprHead) {
s_ptInputOprHead = ptInputOpr;
ptInputOpr->ptNext = NULL;
} else {
ptTmp = s_ptInputOprHead;
while (ptTmp->ptNext)
{
ptTmp = ptTmp->ptNext;
}
ptTmp->ptNext = ptInputOpr;
ptInputOpr->ptNext = NULL;
}
return 0;
}
/* 显示支持拓展文件的名字 */
void ShowInputOpr(void)
{
int i = 0;
PT_InputOpr ptTmp = s_ptInputOprHead;
while (ptTmp) {
printf("%02d %s\n", i++, ptTmp->name);
ptTmp = ptTmp->ptNext;
}
}
/* 初始化函数 */
int InputInit(void)
{
int error;
error = StdinInit();
error |= TouchScreenInit();
return error;
}
/* 调用所有的Input设备的事件发生函数,即轮询的方式获取LCD或标准输入的数据 */
int GetInputEvent(PT_InputEvent ptInputEvent)
{
/* 把链表中的InputOpr的GetInputEvent都调用一次,一旦有数据立即返回 */
PT_InputOpr ptTmp;
ptTmp = s_ptInputOprHead;
while (ptTmp) {
if (ptTmp->GetInputEvent(ptInputEvent) == 0)
return 0;
ptTmp = ptTmp->ptNext;
}
return -1;
}
/* 初始化所有支持的Input设备 */
int AllInputDeviceInit()
{
int error;
PT_InputOpr ptTmp;
error = -1;
ptTmp = s_ptInputOprHead;
while (ptTmp) {
if (ptTmp->DeviceInit() == 0)
error = 0;
ptTmp = ptTmp->ptNext;
}
return 0;
}
3、input设备——stdin
在这个函数中,为stdin
设备进行:分配结构体、设置结构体、注册结构体
/**
* @file stdin.c
* @brief 标准输入input设备的处初始化,处理过程与取消
* @version 1.0 (版本声明)
* @author Dk
* @date July 1,2020
*/
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include "input_manager.h"
/**
* @Description: 获取标准串口输入的初始化,设置模式使其可以采用非阻塞的方式获取输入
* @return 成功:0
*/
static int StdinDevInit(void)
{
struct termios ttystate;
/* 获得终端的状态 */
tcgetattr(STDIN_FILENO, &ttystate);
/* 关闭标准模式,并设置位为 1,表示接受最小用户数输入为1 */
ttystate.c_lflag &= ~ICANON;
ttystate.c_cc[VMIN] = 1;
/* 设置术语状态 */
tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);
return 0;
}
/**
* @Description: 获取标准串口输入的退出,恢复原本的模式
* @return 成功:0
*/
static int StdinDevExit(void)
{
struct termios ttystate;
/* 获得终端的状态 */
tcgetattr(STDIN_FILENO, &ttystate);
/* 打开标准模式 */
ttystate.c_lflag |= ICANON;
/* 设置术语状态 */
tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);
return 0;
}
/**
* @Description: 根据标准输入的结果进行相应处理,采用非阻塞的方式获取输入
* @param ptInputEvent - 表示input设备的结构体.
* @return 描述符fd在描述符集fds中:0 不在:-1
*/
static int StdinGetInputEvent(PT_InputEvent ptInputEvent)
{
struct timeval tv;
fd_set fds;
char c;
/* 输入(stdin)执行无阻塞检查,而不会将超时0 */
tv.tv_sec = 0;
tv.tv_usec = 0;
/* 指定的文件描述符集清空 */
FD_ZERO(&fds);
/* 在文件描述符集合中增加一个新的fds */
FD_SET(STDIN_FILENO, &fds);
/* 测试指定的fds可读 */
select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
/* 测试指定的fds是否在该集合中 */
if (FD_ISSET(STDIN_FILENO, &fds)) {
/* 存在,处理数据 */
ptInputEvent->type = INPUT_TYPE_STDIN;
c = fgetc(stdin);
if (c == 'u') {
ptInputEvent->val = INPUT_VALUE_UP;
gettimeofday(&ptInputEvent->time, NULL);
} else if (c == 'n') {
ptInputEvent->val = INPUT_VALUE_DOWN;
gettimeofday(&ptInputEvent->time, NULL);
} else if (c == 'q') {
ptInputEvent->val = INPUT_VALUE_EXIT;
gettimeofday(&ptInputEvent->time, NULL);
} else {
ptInputEvent->val = INPUT_VALUE_UNKONW;
gettimeofday(&ptInputEvent->time, NULL);
}
return 0;
}else
return -1;
}
static T_InputOpr s_tStdinOpr = {
.name = "stdin",
.DeviceInit = StdinDevInit,
.DeviceExit = StdinDevExit,
.GetInputEvent = StdinGetInputEvent,
};
/**
* @Description: 标准输入初始化函数,供上层调用注册设备
* @return 成功:0
*/
int StdinInit(void)
{
return RegisterInputOpr(&s_tStdinOpr);
}
4、input设备——touchscreen
在这个函数中,为touchscreen
设备进行:分配结构体、设置结构体、注册结构体
/**
* @file touchscreen.c
* @brief touchscreen input设备的处初始化,处理过程与取消,参考tslib中的ts_print.c
* @version 1.0 (版本声明)
* @author Dk
* @date July 1,2020
*/
#include <stdlib.h>
#include <tslib.h>
#include "input_manager.h"
#include "config.h"
#include "draw.h"
static struct tsdev *s_pTSDev; //touchscreen设备
static int s_Xres; //LCD x方向的分辨率
static int s_Xres; //LCD y方向的分辨率
/**
* @Description: 获取touchscreen输入的初始化,设置模式使其可以采用非阻塞的方式获取输入
* @return 成功:0 失败: -1
* @note 由于需要获取到LCD的分辨率,所以这个函数被调用之前,SelectAndInitDisplay必须被调用,才可以获取到数据
*/
static int TouchScreenDevInit(void)
{
char *pTSName = NULL;
/* 根据环境变量获得设备名,并以非阻塞的方式打开 */
if((pTSName = getenv("TSLIB_TSDEVICE")) != NULL)
s_pTSDev = ts_open(pTSName, 1);
else
s_pTSDev = ts_open("/dev/event0", 1);
if (!s_pTSDev) {
DBG_PRINTF("ts_open error!\n");
return -1;
}
if (ts_config(s_pTSDev)) {
DBG_PRINTF("ts_config error!\n");
return -1;
}
/* 获取LCD分辨率 */
if (GetDispResolution(&s_Xres, &s_Xres))
return -1;
return 0;
}
/**
* @Description: touchscreen输入的退出,恢复原本的模式
* @return 成功:0
*/
static int TouchScreenDevExit(void)
{
return 0;
}
/**
* @Description: 判断上一次事件与此次事件相隔的时间是否超过500ms
* @param ppreTime - 存储上一次事件时间的结构体指针,pcurTime - 此次存储事件时间的结构体指针
* @return 两次事件时间间隔超过500ms:0 无超过:1
*/
static int isOutOf500ms(struct timeval *ppreTime, struct timeval *pcurTime)
{
int prems;
int curms;
/* tv_sec - 秒, tv_usec - 微秒 */
prems = ppreTime->tv_sec * 1000 + ppreTime->tv_usec / 1000;
curms = pcurTime->tv_sec * 1000 + pcurTime->tv_usec / 1000;
return (curms > (prems + 500));
}
/**
* @Description: 采用查询的方式获读取touchscreen数据
* @param ptInputEvent - 表示input设备的结构体.
* @return 有数据:0 无数据:-1
*/
static int TouchScreenGetInputEvent(PT_InputEvent ptInputEvent)
{
int ret;
struct ts_sample samp;
static struct timeval preTime;
ret = ts_read(s_pTSDev, &samp, 1);
/* 无数据返回 */
if (ret < 0) {
DBG_PRINTF("ts_read no data!\n");
return -1;
}
/* 有数据处理 */
if ((isOutOf500ms(&preTime, &samp.tv))) {
/* 两次事件时间间隔超过500ms */
ptInputEvent->type = INPUT_TYPE_TOUCHSCREEN;
preTime = samp.tv;
ptInputEvent->time = samp.tv;
/* 根据读取到的LCD y坐标值,进行对应操作
* y < y分辨率的1/3,则为上翻
* y > y分辨率的2/3,则为下翻
* 其他情况,则为未定义操作
*/
if (samp.y < (s_Xres / 3))
ptInputEvent->val = INPUT_VALUE_UP;
else if (samp.y > (2 * s_Xres / 3))
ptInputEvent->val = INPUT_VALUE_DOWN;
else
ptInputEvent->val = INPUT_VALUE_UNKONW;
return 0;
} else
return -1;
return 0;
}
static T_InputOpr s_tTouchScreenOpr = {
.name = "touchscreen",
.DeviceInit = TouchScreenDevInit,
.DeviceExit = TouchScreenDevExit,
.GetInputEvent = TouchScreenGetInputEvent,
};
/**
* @Description: touchscreen input设备初始化函数,供上层调用注册设备
* @return 成功:0
*/
int TouchScreenInit(void)
{
return RegisterInputOpr(&s_tTouchScreenOpr);
}
四、修改
1、main.c文件修改
1.1 显示支持信息部分
- 修改前:
```c
/* 显示当前支持信息 */
if (List)
{
printf("supported display:\n");
ShowDispOpr();
printf("supported font:\n");
ShowFontOpr();
printf("supported encoding:\n");
ShowEncodingOpr();
return 0;
}
- 修改后:
/* 显示当前支持信息 */
if (List)
{
printf("supported display:\n");
ShowDispOpr();
printf("supported font:\n");
ShowFontOpr();
printf("supported encoding:\n");
ShowEncodingOpr();
printf("supported input:\n");
ShowInputOpr();
return 0;
}
1.2 循环部分
- 修改前:只支持串口标准输入控制
/* 循环根据输入信息进行对应操作
* n: 显示下一页信息
* u: 显示上一页信息
* q: 退出程序
*/
while (1)
{
printf("Enter 'n' to show next page, 'u' to show previous page, 'q' to exit: ");
do {
Opr = getchar();
} while ((Opr != 'n') && (Opr != 'u') && (Opr != 'q'));
if (Opr == 'n')
ShowNextPage();
else if (Opr == 'u')
ShowPrePage();
else
return 0;
}
- 修改后:通过轮询的方式,调用
GetInputEvent(&InputEvent)
,不断的判断串口和LCD是否有输入,根据输入的标志位来进行相关操作。
/* 循环根据输入信息进行对应操作
* n: 显示下一页信息
* u: 显示上一页信息
* q: 退出程序
*/
printf("Enter 'n' to show next page, 'u' to show previous page, 'q' to exit\n\r");
printf("Touch the upper third of the touch screen to show previous page\n\r");
printf("Touch the bottom third of the touch screen to show next page\n\r");
while (1)
{
if (GetInputEvent(&InputEvent) == 0) {
if (InputEvent.val == INPUT_VALUE_DOWN)
ShowNextPage();
else if (InputEvent.val == INPUT_VALUE_UP)
ShowPrePage();
else if (InputEvent.val == INPUT_VALUE_EXIT) {
printf("\n\r");
return 0;
}
}
}
2、draw.c文件修改
2.1 初始化部分修改
- 修改前:
int DrawInit(void)
{
int error;
/* 初始化 */
error = DisplayInit();
if (error) {
printf("DisplayInit error!\n");
return -1;
}
error = FontsInit();
if (error) {
printf("FontsInit error!\n");
return -1;
}
error = EncodingInit();
if (error) {
printf("EncodingInit error!\n");
return -1;
}
return 0;
}
- 修改后:添加了input部分的初始化
int DrawInit(void)
{
int error;
/* 初始化 */
error = DisplayInit();
if (error) {
printf("DisplayInit error!\n");
return -1;
}
error = FontsInit();
if (error) {
printf("FontsInit error!\n");
return -1;
}
error = EncodingInit();
if (error) {
printf("EncodingInit error!\n");
return -1;
}
error = InputInit();
if (error) {
printf("InputInit error!\n");
return -1;
}
return 0;
}
3、Makefile修改
3.1 /input目录下Makefile
# 子目录Makefile
obj-y += input_manager.o
obj-y += stdin.o
obj-y += touchscreen.o
3.2 顶层目录Makefile
- 添加库:
LDFLAGS := -lm -lfreetype -lts
- 添加input目录:
obj-y += input/
- 完整文件:
# 顶层目录Makefile
#
# 目的:
# 1、每个子目录都会建立一个Makefile,包含当前目录下.c文件.h文件
# 2、顶层目录下
# 交叉编译工具链
CROSS_COMPILE = arm-linux-
# 定义一些变量 $(CROSS_COMPILE):取出该变量的值 $(CROSS_COMPILE)gcc->arm-linux-gcc
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 方便您利用隐含规则指定编译C语言源程序的旗标
# -Wall 帮助列出所有警告信息
# -02 多优化一些,除了涉及空间和速度交换的优化选项,执行几乎所有的优化工作
# -g 可以认为它是缺省推荐的选项
CFLAGS := -Wall -O2 -g
# 把当前目录下的include目录下指定为系统目录
# += 为已经定以过的变量的值追加更多的文本
# -I 指定搜寻包含makefile文件的路径
# $(shell pwd) 执行shell命令的pwd命令,获取执行命令的结果
CFLAGS += -I $(shell pwd)/include
# LDFLAGS 用于调用linker(‘ld’)的编译器的额外标志
# -l 连接库的搜寻目录 -lm调用math库 -lfreetype调用freetype库
LDFLAGS := -lm -lfreetype -lts
# 取出变量的值
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
# TARGET 目标变量
# := 简单扩展型变量的值是一次扫描永远使使用
TARGET := show_file
obj-y += main.o
obj-y += display/
obj-y += draw/
obj-y += encoding/
obj-y += fonts/
obj-y += input/
# 规则 arm-linux-gcc -lm -lfreetype -o show_file built-in.o
all :
make -C ./ -f $(TOPDIR)/Makefile.build
$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
# clean 删除所有make正常创建的文件
# 规则 找到所有make创建出来.o文件进行删除
# 删除show_file
clean :
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
# distclean 删除所有正常创建的文件
# 配置文件或为编译正常创建的准备文件,甚至makefile文件自身不能创建的文件
# 规则 找到所有mkae创建出来的.o文件进行删除
# 删除show_file
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
五、编译与运行
1、编译
执行make
,得到可执行文件show_file
2、运行
由于使用到触摸屏,需要调用tslib
库来进行校准
- 执行
./show_file -l
,显示出当前支持的设备
- 执行
./shoe_file -s 16 -h HZK16 -f ./MSYH.TTF hz.txt
通过串口输入,可以立刻根据输入的值进行操作,不需要回车键后才进行操作;
通过触摸屏输入,点击屏幕的上1/3部分进行上页显示,点击屏幕的下1/3部分进行下页显示。