嵌入式应用实例→电子产品量产工具→输入系统的输入管理代码分析和上机测试记录(互斥锁、进程的挂起和唤醒、环形缓冲区等知识点)

前言

因为系统中有两个输入设备(触摸屏输入设备+网络输入设备),所以就牵涉到下面两个问题:
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)的满状态检测,其逻辑如下:

环形缓冲区基础

  1. 定义: 环形缓冲区是一种逻辑上首尾相连的数组结构,用于数据的循环存储。
  2. 两个指针:
    • g_iWrite: 写指针,指向下一个写入数据的位置。
    • g_iRead: 读指针,指向下一个读取数据的位置。
  3. 环形特点: 使用模运算 (% BUFFER_LEN) 保证指针在到达数组尾部后回到开头,形成循环。

判断满状态的逻辑

环形缓冲区满的条件是:如果再写入一个数据,就会覆盖尚未读取的数据。为避免覆盖,通常预留一个空位作为缓冲区满的标志

逻辑推导
  • (g_iWrite + 1) % BUFFER_LEN 表示写指针向前移动一个位置。
  • 当写指针的“下一个位置”(即 (g_iWrite + 1) % BUFFER_LEN)与读指针 g_iRead 重合时,说明环形缓冲区满了。
  • 换句话说:如果再写入一个数据,写指针就会覆盖读指针所指向的数据(尚未读取),这是不允许的。
伪代码描述
if (g_iRead == ((g_iWrite + 1) % BUFFER_LEN)) {
    // 缓冲区满
}
优点

这种设计中,环形缓冲区始终保留一个空位,用于区分以下三种状态:

  1. 缓冲区为空:g_iWrite == g_iRead
  2. 缓冲区满:g_iRead == ((g_iWrite + 1) % BUFFER_LEN)
  3. 缓冲区非空非满:其他情况。

注意事项

  • 缓冲区的容量有效数据量为 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__); 是一种调试手段,常用于输出程序运行时的文件名、函数名和代码行号,以便于追踪程序的执行路径和定位问题。

参数说明:

  1. __FILE__:

    • 是一个预定义的宏,表示当前代码所在的源文件名。
    • 输出的内容是文件的完整路径或文件名,取决于编译器的实现。
  2. __FUNCTION__:

    • 是一个扩展的预定义标识符,表示当前代码所在的函数名。
    • 在调试和日志记录中,可以显示执行该代码片段的具体函数名称。
  3. __LINE__:

    • 是一个预定义的宏,表示当前代码行号。
    • 输出的内容是代码中该语句所在的具体行号。

作用:

  • 调试信息输出:
    • 输出文件名、函数名和行号,帮助开发者确认当前代码执行的位置。
  • 运行路径跟踪:
    • 如果程序在复杂的逻辑中运行,输出这些信息可以帮助理解程序执行的顺序。
  • 快速问题定位:
    • 当程序崩溃或产生错误时,通过输出的文件名、函数名和行号,可以迅速定位问题发生的位置。

示例输出:

假设这段代码位于文件 main.c 的 15 行,函数 main 中。运行时会输出类似:

main.c main 15

每次执行到这句 printf,它都会输出当时的文件名、函数名和行号,动态反映程序的执行进度。

为什么放在这里?

在你的代码中:

  1. 这是在进入主循环 while (1) 后的第一条打印语句。
  2. 它的作用是记录程序是否顺利进入了 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"

在这里插入图片描述
测试成功。

附完整源代码

https://pan.baidu.com/s/1Bm27-B2mh1pbEVrn7TJJuQ?pwd=gyce

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值