數碼相冊——電子書輪詢方式支持多輸入
- 硬件平臺:韋東山嵌入式Linxu開發板(S3C2440.v3)
- 軟件平臺:運行於VMware Workstation 12 Player下UbuntuLTS16.04_x64 系統
- 參考資料:《嵌入式Linux應用開發手冊》、《嵌入式Linux應用開發手冊第2版》、《gcc中文手冊》
- 開發環境:Linux 3.4.2內核、arm-linux-gcc 4.3.2工具鏈
目錄
一、前言
在【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部分進行下頁顯示。