前言
因为系统中有两个输入设备(触摸屏输入设备+网络输入设备),所以就牵涉到下面两个问题:
1、设备的注册管理和初始化
2、多输入设备怎么样保证输入时不引起冲突、数据不丢失。
输入设备的管理系统主要就为解决上面两个问题。
触摸屏输入设备的相关学习记录:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144609033
https://blog.youkuaiyun.com/wenhao_ir/article/details/144621008
网络输入设备的相关学习记录:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144659003
https://blog.youkuaiyun.com/wenhao_ir/article/details/144659643
输入设备的注册管理和初始化
访照 电子产品量产工具→显示系统的代码 的逻辑来进行输入设备的注册和管理。
第01步-初始化各个设备的InputDevice
类型的结构体
在各设备的底层代码中初始化一个InputDevice
类型的结构体:
input\touchscreen.c
中的相关代码如下:
static InputDevice g_tTouchscreenDev ={
.name = "touchscreen",
.GetInputEvent = TouchscreenGetInputEvent,
.DeviceInit = TouchscreenDeviceInit,
.DeviceExit = TouchscreenDeviceExit,
};
input\netinput.c
中的相关代码如下:
static InputDevice g_tNetinputDev ={
.name = "touchscreen",
.GetInputEvent = NetinputGetInputEvent,
.DeviceInit = NetinputDeviceInit,
.DeviceExit = NetinputDeviceExit,
};
第02步-书写输入设备管理层的设备注册函数
InputDevice
类型的结构体存储着设备的基本信息,比如名字、初始函数、退出函数、输入事件处理函数。有了设备的这个结构体,便可进行注册了,所以我们在输入管理代码中需要有注册函数。
在输入管理代码input\input_manager.c
有注册函数的代码如下:
void RegisterInputDevice(PInputDevice ptInputDev)
{
ptInputDev->ptNext = g_InputDevs;
g_InputDevs = ptInputDev;
}
可见,注册,主要是对设备结构体PInputDevice
的ptNext成员进行操作,使所有的设备通过结构体链接连接起来。
第03步-书写各个设备单独的注册函数
在输入管理代码input\input_manager.c
有了注册函数后,我们便可以在各个设备的底层代码中写出各自的注册函数了,本质上就是把各自的PInputDevice实例作为参数来调用函数RegisterInputDevice(),各自的代码如下:
input\touchscreen.c
中的相关代码如下:
void TouchscreenRegister(void)
{
RegisterInputDevice(&g_tTouchscreenDev);
}
input\netinput.c
中的相关代码如下:
void NetInputRegister(void)
{
RegisterInputDevice(&g_tNetinputDev);
}
第04步-书写对各个设备进行注册的函数
在各个设备有了自己的注册函数之后,在输入设备管理代码中便可以对所有的设备进行注册了,相关代码如下:
input\input_manager.c
void InputRegister(void)
{
/* regiseter touchscreen */
extern void TouchscreenRegister(void);
TouchscreenRegister();
/* regiseter netinput */
extern void NetInputRegister(void);
NetInputRegister();
}
这里我需要说明一下:
①这里的extern其实是不需要的,正确的操作是把函数TouchscreenRegister()和函数NetInputRegister()写在头文件中进行声音,在C语言中,函数的声明自带extern属性。
②开发板教程里和提供的源码里这里的函数名InputInit不好,准确的名字其实应该是InputRegister
,所以我进行了更改,并且把相关涉及到部分也改了。
第05步-设备初始化操作并创建相应的线程
注册完成后,我们便可以对各个设备进行初始化操作了,从设备结构体PInputDevice的链表里取出各个设备的结构体PInputDevice实例,然后进行设备的初始化操作,这就需要涉及到结构体链表的遍历。
相关代码如下:
input\input_manager.c
void IntputDeviceInit(void)
{
int ret;
pthread_t tid;
/* for each inputdevice, init, pthread_create */
PInputDevice ptTmp = g_InputDevs;
while (ptTmp)
{
/* init device */
ret = ptTmp->DeviceInit();
/* pthread create */
if (!ret)
{
ret = pthread_create(&tid, NULL, input_recv_thread_func, ptTmp);
}
ptTmp= ptTmp->ptNext;
}
}
注意:教程里把这个函数的名字写成了IntpuDeviceInit
,中间少了一个字母t,我已进行更正。
如果设备初始化成功,我们便利用函数pthread_create()去为每个设备创建其对应的线程 。关于线程的概念和函数pthread_create()的介绍,请参考博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144698663
在利用函数pthread_create()创建相应的线程的时候,就涉及到了线程的执行函数,在这里是函数input_recv_thread_func(),所以我们就要去好好看下线程的执行函数input_recv_thread_func()
执行了哪些操作。其实这一部分也是在解决前言中提到的第2个问题:“输入管理程序要保证输入时不引起冲突、数据不丢失。”
保证多个输入设备输入数据时的不冲突、数据不丢失
关于线程的相关知识请参考博文,建议看下面的内容前先把下面这篇博文看一下:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144698663
原理分析
我们设置一个缓冲区,这个缓冲区用于存储读到的各个输入设备输入的数据,并从这个缓冲区读出数据,显然在读这个缓冲区时,任意时刻只能有一个线程进行操作;同样,在写这个缓冲区时,任意时刻也只能有一个线程进行操作。即读和写都存在线程间的竞争问题!
为了实现这样的效果,我们需要使用线程互斥锁的相关机制并配合线程的挂起机制去实现。
线程执行函数input_recv_thread_func()
的分析
static void *input_recv_thread_func (void *data)
{
PInputDevice ptInputDev = (PInputDevice)data;
InputEvent tEvent;
int ret;
while (1)
{
/* 读数据 */
ret = ptInputDev->GetInputEvent(&tEvent);
if (!ret)
{
/* 保存数据 */
pthread_mutex_lock(&g_tMutex);
PutInputEventToBuffer(&tEvent);
pthread_mutex_unlock(&g_tMutex);
/* 唤醒等待数据的线程 */
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
}
}
return NULL;
}
这个函数的代码第1句关键语句:
ret = ptInputDev->GetInputEvent(&tEvent);
可见我们能得去看函数GetInputEvent()
的实现,注意,这里的函数并不是文件\input\input_manager.c
中的函数GetInputEvent()
,而是各个设备的InputDevice结构体的GetInputEvent成员对应的函数,具体来说,在这里,触摸屏设备是函数TouchscreenGetInputEvent
,网络设备是函数NetinputGetInputEvent()
。
各个设备的GetInputEvent成员对应的函数都是如果读到了数,会返回0值,所以当某个线程读到数据后,则执行下面的操作:
/* 保存数据 */
pthread_mutex_lock(&g_tMutex);
PutInputEventToBuffer(&tEvent);
pthread_mutex_unlock(&g_tMutex);
/* 唤醒等待数据的线程 */
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
首先把互斥锁g_tMutex进行加锁操作,因为函数PutInputEventToBuffer()
的操作是一种竞争操作,数据通过过函数PutInputEventToBuffer()
的放入缓冲区后,再解锁互斥锁g_tMutex,然后再把之前因为没有读到数据的线程而挂起。
如果函数GetInputEvent()
没有读到数据,则while循环中不作任何操作。
说明一下,在开发板配套的源码中,上面这段代码的顺序如下:
/* 保存数据 */
pthread_mutex_lock(&g_tMutex);
PutInputEventToBuffer(&tEvent);
/* 唤醒等待数据的线程 */
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
pthread_mutex_unlock(&g_tMutex);
我觉得代码:
pthread_cond_signal(&g_tConVar); /* 通知接收线程 */
pthread_mutex_unlock(&g_tMutex);
的顺序不对,应该是先解锁,再去通知释放相应的线程,否则你通知相应的挂起的线程该执行了,结果它在获取互斥锁时,有可能发现无法获取,这是存在问题的。
读取数结构体处理函数GetInputEvent()
的分析
在文件unittest\input_test.c
中,写了测试这个输入设备管理代码的测试代码,里面就一个主函数,主函数进入循环体后第一句代码便是:
ret = GetInputEvent(&event);
这里就调用了我们这里要分析的函数GetInputEvent()
,其代码如下;
int GetInputEvent(PInputEvent ptInputEvent)
{
InputEvent tEvent;
int ret;
/* 无数据则休眠 */
pthread_mutex_lock(&g_tMutex);
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
pthread_mutex_unlock(&g_tMutex);
return 0;
}
else
{
/* 休眠等待 */
pthread_cond_wait(&g_tConVar, &g_tMutex);
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
ret = 0;
}
else
{
ret = -1;
}
pthread_mutex_unlock(&g_tMutex);
}
return ret;
}
读数据处理函数GetInputEvent()
的第1句关键代码:
pthread_mutex_lock(&g_tMutex);
这里是对互斥锁g_tMutex
进行上锁,对线程互斥锁的理解请看我的另一篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144698663
接着的代码就是去数据缓冲区读取数据了:
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
pthread_mutex_unlock(&g_tMutex);
return 0;
}
else
{
/* 休眠等待 */
pthread_cond_wait(&g_tConVar, &g_tMutex);
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
ret = 0;
}
else
{
ret = -1;
}
pthread_mutex_unlock(&g_tMutex);
}
return ret;
这段代码的分析:
首先要去函数GetInputEventFromBuffer()
中读取数据缓冲区的分析,关于函数GetInputEventFromBuffer()
的分析见本文后面。
从后面对函数GetInputEventFromBuffer()
的分析中可知,如果函数GetInputEventFromBuffer()
读到了数据缓冲区的数据,则返回值为1,否则返回值为0。
如果读到了缓冲区的数据,则把读到的InputEvent结构体存储在变量ptInputEvent
中,并且解锁互斥锁g_tMutex。相关代码如下:
if (GetInputEventFromBuffer(&tEvent))
{
*ptInputEvent = tEvent;
pthread_mutex_unlock(&g_tMutex);
return 0;
}
假如没有读到数据,那么该函数对应的线程(实际上就是main函数对应的线程,因为是main函数调用这个函数)的执行进入挂起状态,即else分支中的语句:
pthread_cond_wait(&g_tConVar, &g_tMutex);
当有线程读到数据后,唤醒这个主函数所在的线程,这个线程唤醒后首先再去读一次数据,如果发现读到了数据结构,则把读到的InputEvent结构体存储在变量ptInputEvent
中,并把返回值ret置为0,如果还是没有读到(按道理应该是要读到数据的,因为有数据写入这个线程才会被唤醒呀,所以如果没有读到,那就是出现了异常),则把返回值ret置为-1,最后把ret的值作为返回值返回。
这个函数的流程靠文字描述不太好理解,可以结合我写的程序大体流程来理解,链接:
https://kdocs.cn/l/cbh6GruwNm7o
数据缓冲区读取数据结构体的函数GetInputEventFromBuffer()
static int GetInputEventFromBuffer(PInputEvent ptInputEvent)
{
if (!isInputBufferEmpty())
{
*ptInputEvent = g_atInputEvents[g_iRead];
g_iRead = (g_iRead + 1) % BUFFER_LEN;
return 1;
}
else
{
return 0;
}
}
缓冲区不为空
在这个代码中,首先去判断缓冲区是否为空:
if (!isInputBufferEmpty())
所以要去看函数isInputBufferEmpty()
,函数isInputBufferEmpty()
的分析见本文后面。
如果isInputBufferEmpty()
的返回值为0,则代表缓冲区不为空,于是执行if分支:
*ptInputEvent = g_atInputEvents[g_iRead];
g_iRead = (g_iRead + 1) % BUFFER_LEN;
return 1;
要读懂这里的代码:
*ptInputEvent = g_atInputEvents[g_iRead];
先看结构体ptInputEvent
的定义:
typedef struct InputEvent {
struct timeval tTime;
int iType;
int iX;
int iY;
int iPressure;
char str[1024];
}InputEvent, *PInputEvent;
还要注意到g_atInputEvents是一个全局变量,并且是InputEvent类似的数组:
static InputEvent g_atInputEvents[BUFFER_LEN];
g_atInputEvents数组中每个就是一个InputEvent
类型的结构体成员。
所以这里缓冲区相当于就是g_atInputEvents数组,这个数组里的每个成员就是一个InputEvent
类型的结构体成员,显然每个成员都存储着某个输入设备的某次输入信息,这个数组的大小由宏定义BUFFER_LEN
决定:
#define BUFFER_LEN 20
当缓冲区不为空时,读取g_atInputEvents中位置为g_iRead的成员,然后赋值给ptInputEvent
,并把位置变量进行如下的+1处理:
g_iRead = (g_iRead + 1) % BUFFER_LEN;
这是显然进行了加1处理,即指向下一个缓冲数据的位置,并且每当读到第20个时,便又回到第0个位置,这就是所谓的环形BUFFER。
缓冲区为空
当缓冲区为空时,isInputBufferEmpty()
的返回值为1,取反后为0,所以此时执行else分支:
else
{
return 0;
}
即,此时GetInputEventFromBuffer
的返回值为0。
判断缓冲区是否为空的函数isInputBufferEmpty()
的分析
static int isInputBufferEmpty(void)
{
return (g_iRead == g_iWrite);
}
可见就一句话,即判断全局变量g_iRead、g_iWrite是否相等,这两个全局变量的定义如下:
static int g_iRead = 0;
static int g_iWrite = 0;
假如这两个值相等,说明缓冲区为空,此时返回1,否则返回0。
这两个变量的值之所以变化在前面分析的函数GetInputEventFromBuffer()中。
将读取到的数据结构体写入缓冲数组中的函数PutInputEventToBuffer()分件
static void PutInputEventToBuffer(PInputEvent ptInputEvent)
{
if (!isInputBufferFull())
{
g_atInputEvents[g_iWrite] = *ptInputEvent;
g_iWrite = (g_iWrite + 1) % BUFFER_LEN;
}
}
这个函数首先判断下数据结构体缓存数组满没满,如果没有满,则写入数据结构体到缓存数组,否则不执行任何操作,即丢弃。这里要特别注意函数isInputBufferFull()
判断环形缓冲区满的逻辑,详见对函数isInputBufferFull()
的分析。
判断环形缓冲区是否满的函数isInputBufferFull()
static int isInputBufferFull(void)
{
return (g_iRead == ((g_iWrite + 1) % BUFFER_LEN));
}
这个判断逻辑其实是不好理解的,详解如下:
语句 g_iRead == ((g_iWrite + 1) % BUFFER_LEN)
通常用于环形缓冲区(Circular Buffer)的满状态检测,其逻辑如下:
环形缓冲区基础
- 定义: 环形缓冲区是一种逻辑上首尾相连的数组结构,用于数据的循环存储。
- 两个指针:
g_iWrite
: 写指针,指向下一个写入数据的位置。g_iRead
: 读指针,指向下一个读取数据的位置。
- 环形特点: 使用模运算 (
% BUFFER_LEN
) 保证指针在到达数组尾部后回到开头,形成循环。
判断满状态的逻辑
环形缓冲区满的条件是:如果再写入一个数据,就会覆盖尚未读取的数据。为避免覆盖,通常预留一个空位作为缓冲区满的标志。
逻辑推导
(g_iWrite + 1) % BUFFER_LEN
表示写指针向前移动一个位置。- 当写指针的“下一个位置”(即
(g_iWrite + 1) % BUFFER_LEN
)与读指针g_iRead
重合时,说明环形缓冲区满了。 - 换句话说:如果再写入一个数据,写指针就会覆盖读指针所指向的数据(尚未读取),这是不允许的。
伪代码描述
if (g_iRead == ((g_iWrite + 1) % BUFFER_LEN)) {
// 缓冲区满
}
优点
这种设计中,环形缓冲区始终保留一个空位,用于区分以下三种状态:
- 缓冲区为空:
g_iWrite == g_iRead
- 缓冲区满:
g_iRead == ((g_iWrite + 1) % BUFFER_LEN)
- 缓冲区非空非满:其他情况。
注意事项
- 缓冲区的容量有效数据量为
BUFFER_LEN - 1
,因为必须预留一个空位作为状态区分。 - 在实际实现中,需确保所有对指针的操作都是线程安全的,避免竞争条件导致状态判断错误。
测试文件unittest\input_test.c
的分析(测试main函数的分析)
测试文件unittest\input_test.c
就只有一个主函数。
测试主函数源代码
int main(int argc, char **argv)
{
int ret;
InputEvent event;
InputRegister();
IntputDeviceInit();
while (1)
{
printf("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
ret = GetInputEvent(&event);
printf("%s %s %d, ret = %d\n", __FILE__, __FUNCTION__, __LINE__, ret);
if (ret) {
printf("GetInputEvent err!\n");
return -1;
}
else
{
printf("%s %s %d, event.iType = %d\n", __FILE__, __FUNCTION__, __LINE__, event.iType );
if (event.iType == INPUT_TYPE_TOUCH)
{
printf("Type : %d\n", event.iType);
printf("iX : %d\n", event.iX);
printf("iY : %d\n", event.iY);
printf("iPressure : %d\n", event.iPressure);
}
else if (event.iType == INPUT_TYPE_NET)
{
printf("Type : %d\n", event.iType);
printf("str : %s\n", event.str);
}
}
}
return 0;
}
代码printf("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
的分析
在代码中,printf("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
是一种调试手段,常用于输出程序运行时的文件名、函数名和代码行号,以便于追踪程序的执行路径和定位问题。
参数说明:
-
__FILE__
:- 是一个预定义的宏,表示当前代码所在的源文件名。
- 输出的内容是文件的完整路径或文件名,取决于编译器的实现。
-
__FUNCTION__
:- 是一个扩展的预定义标识符,表示当前代码所在的函数名。
- 在调试和日志记录中,可以显示执行该代码片段的具体函数名称。
-
__LINE__
:- 是一个预定义的宏,表示当前代码行号。
- 输出的内容是代码中该语句所在的具体行号。
作用:
- 调试信息输出:
- 输出文件名、函数名和行号,帮助开发者确认当前代码执行的位置。
- 运行路径跟踪:
- 如果程序在复杂的逻辑中运行,输出这些信息可以帮助理解程序执行的顺序。
- 快速问题定位:
- 当程序崩溃或产生错误时,通过输出的文件名、函数名和行号,可以迅速定位问题发生的位置。
示例输出:
假设这段代码位于文件 main.c
的 15 行,函数 main
中。运行时会输出类似:
main.c main 15
每次执行到这句 printf
,它都会输出当时的文件名、函数名和行号,动态反映程序的执行进度。
为什么放在这里?
在你的代码中:
- 这是在进入主循环
while (1)
后的第一条打印语句。 - 它的作用是记录程序是否顺利进入了
while (1)
循环,以及当前执行到哪一行。
这种打印方式对程序调试尤为有用,特别是在多文件、多函数调用的复杂代码中。
语句ret = GetInputEvent(&event);
这句话就是去调用input\input_manager.c中的函数GetInputEvent()去读取环形缓冲区的数据。函数GetInputEvent()之前已经详细分析了。
值得注意的是,当函数GetInputEvent()因为没有读到数据而处于挂起状态时,main()函数由于调用了函数GetInputEvent(),也会一并处于挂起状态哦。
读取数据错误处理代码
if (ret) {
printf("GetInputEvent err!\n");
return -1;
}
当函数GetInputEvent()因为有线程写入数据而被释放后,去读取数据,结果还是没有读到任何数据,这个时候ret的值就是-1,这种情况就是错误情况,此时打印错误信息并退出程序。
main函数后续的代码
后续的代码:
else
{
printf("%s %s %d, event.iType = %d\n", __FILE__, __FUNCTION__, __LINE__, event.iType );
if (event.iType == INPUT_TYPE_TOUCH)
{
printf("Type : %d\n", event.iType);
printf("iX : %d\n", event.iX);
printf("iY : %d\n", event.iY);
printf("iPressure : %d\n", event.iPressure);
}
else if (event.iType == INPUT_TYPE_NET)
{
printf("Type : %d\n", event.iType);
printf("str : %s\n", event.str);
}
没啥好说的,就是把读到的数据结构根据输入设备的类型打印出来。
程序大体流程图
程序大体流程图见链接:
https://kdocs.cn/l/cbh6GruwNm7o
一些自己提出的思考
01-问题1:结构体InputEvent的命名是否准确?
答:不准确,这个结构体实际上是存储从输入设备得到来的数据,而不是输入事件,准确的名称应该叫:InputDeviceData
或者InputEventData
。
交叉编译
注意:Makefile文件中的链接器设置要加上thread
库:
LDFLAGS := -lts -lpthread
代码复制到Ubuntu中
交叉编译过程略…
重命名生成的test二进制可执行文件。
上板测试
之前编译好的网络输入设备的client端程序也要一并用上哦,所以不妨先把这个client端程序放于NFS文件系统中。
打开开发板,挂载网络文件:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
添加执行权限:
chmod +x inputmanger_test
chmod +x net_client_test
后台运行程序 inputmanger_test
./inputmanger_test &
用命令ps
和命令ls /proc/<PID>/task/
看下当前的线程情况:
可见有三个线程,2180是主线程,2181和2183是两个输入设备的线程。
此时点击下触摸屏,看能不能获得触摸屏的输入信息:
可见,获得输入信息了。
接下来,运行网络输入设备的客户端程序:
./net_client_test 127.0.0.1 "my name is SuWenhao"
测试成功。