Linux驱动开发(十)---树莓派输入子系统学习(红外接收)

前文回顾

《Linux驱动开发(一)—环境搭建与hello world》
《Linux驱动开发(二)—驱动与设备的分离设计》
《Linux驱动开发(三)—设备树》
《Linux驱动开发(四)—树莓派内核编译》
《Linux驱动开发(五)—树莓派设备树配合驱动开发》
《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
《Linux驱动开发(七)—树莓派按键驱动开发》
《Linux驱动开发(八)—树莓派SR04驱动开发》
《Linux驱动开发(九)—树莓派I2C设备驱动开发(BME280)》

继续宣传一下韦老师的视频

70天30节Linux驱动开发快速入门系列课程【实战教学、技术讨论、直播答疑】

在这里插入图片描述
这次借助学习红外遥控的学习,学习一下输入子系统的相关知识。

硬件相关

用的是一个vs1838的模块接收,开发的就是接收程序的驱动,发送用普通遥控器即可。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第1张图片
用到的协议是NEC协议,简单介绍一下
NEC编码的一帧(通常按一下遥控器按钮所发送的数据)由引导码、地址码及数据码组成,,如下图所示,把地址码及数据码取反的作用是加强数据的正确性。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第2张图片
引导码及数据的定义如下图所示,当一直按住一个按钮的时候,会隔110ms左右发一次引导码(重复),并不带任何数据
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第3张图片
这些不是今天的重点,所以只需要了解即可。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第4张图片

连接的方法,就是三根线,数据,VCC和GND,还是用我们熟悉的GPIO17

设备树

这个设备树与按键的设备树一样,只有一个GPIO作为输入使用。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第5张图片
通过在GPIO上设置中断,捕获中断的时间,然后根据前面的NEC协议,来解析出每个字节的数据,并进行校验。
当然,这也不是今天的重点。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第6张图片

驱动中的重点内容

模块挂载,卸载这些就不说了,都一样。主要是在probe函数中,不再去注册设备,而是改为增加对输入子系统的使用。

static int vs1838_probe(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 1. 获得硬件信息 */
	vs1838_data_pin = gpiod_get(&pdev->dev, NULL, 0);
	if (IS_ERR(vs1838_data_pin))
	{
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	}

	irq = gpiod_to_irq(vs1838_data_pin);

	request_irq(irq, vs1838_isr, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, "vs1838", NULL);

	/* 输入系统的代码 */
	/* 参考: drivers\input\keyboard\gpio_keys.c */
	/* A. 分配input_dev */
	vs1838_input_dev = devm_input_allocate_device(&pdev->dev);

	/* B. 设置input_dev */
	vs1838_input_dev->name = "vs1838";
	vs1838_input_dev->phys = "vs1838";

	/* B.1 能产生哪类事件 */
	__set_bit(EV_KEY, vs1838_input_dev->evbit);
	__set_bit(EV_REP, vs1838_input_dev->evbit);
	
	/* B.2 能产生哪些事件 */
	//__set_bit(KEY_0, vs1838_input_dev->keybit);
	memset(vs1838_input_dev->keybit, 0xff, sizeof(vs1838_input_dev->keybit));
	
	/* C. 注册input_dev */
	input_register_device(vs1838_input_dev);

	return 0;
}

理解一下这段代码

  1. 分配input_dev 并设置参数
vs1838_input_dev = devm_input_allocate_device(&pdev->dev);
vs1838_input_dev->name = "vs1838";
vs1838_input_dev->phys = "vs1838";
  1. 能产生哪类事件
    设备驱动通过set_bit()告诉input子系统它支持哪些事件
set_bit(EV_KEY, vs1838_input_dev->evbit);
set_bit(EV_REP, vs1838_input_dev->evbit);

主要的事件类型包括

按键 说明
EV_SYN 0x00 事件间的分割标志,有些事件可能会在时间和空间上产生延续,比如持续按住一个按键。为了更好地管理这些持续的事件,EV_SYN 用以将他们分割成一个个的小的数据包
EV_KEY 0x01 按键事件
EV_REL 0x02 相对值,如光标移动,报告的是相对最后一次位置的偏移
EV_ABS 0x03 绝对值,如触摸屏和操纵杆,它们工作在绝对坐标系统
EV_MSC 0x04 不能匹配现有的类型,这相当于当前暂不识别的事件。比如在 Linux系统中按下键盘中针对 Windows 系统的“一键杀毒”按键,将会产生该事件
EV_SW 0x05 显示设备开关状态
EV_LED 0x11 用于控制设备上的 LED 灯的开关,比如按下键盘的大写锁定键,会同时产生 ”EV_KEY” 和 ”EV_LED” 两个事件
EV_SND 0x12 显示声音效果
EV_REP 0x14 重复
EV_FF 0x15 力反馈事件
EV_PWR 0x16 电源事件
EV_FF_STATUS 0x17 力反馈状态事件
EV_MAX 0x1f
EV_CNT (EV_MAX+1)

Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第7张图片

  1. 能产生哪些事件 ,0xff类似于掩码,表示按键值可以0~0xff。
memset(vs1838_input_dev->keybit, 0xff, sizeof(vs1838_input_dev->keybit));
  1. 注册input_dev

这些操作完成后,就会存在一个新的event文件。存在于/dev/input路径下
在这里插入图片描述

input_register_device(vs1838_input_dev);

最后,在获得到正确的button值之后,需要产生通知,val是按键值,上报一个确认并且上报一个停止

input_event(vs1838_input_dev, EV_KEY, val, 1);
input_event(vs1838_input_dev, EV_KEY, val, 0);
input_sync(vs1838_input_dev);	

并且我们的程序可以很简单。
以下是完整代码

#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


static struct gpio_desc *vs1838_data_pin;
static int irq;
static unsigned int vs1838_data = 0;  

static u64 vs1838_edge_time[100];
static int vs1838_edge_cnt = 0;

static struct input_dev *vs1838_input_dev;


/* 0 : 成功, *val中记录数据
 * -1: 没接收完毕
 * -2: 解析错误
 */
int vs1838_parse_data(unsigned int *val)
{
	u64 tmp;
	unsigned char data[4];
	int i, j, m;
	
	/* 判断是否重复码 */
	if (vs1838_edge_cnt == 4)
	{
		tmp = vs1838_edge_time[1] - vs1838_edge_time[0];
		if (tmp > 8000000 && tmp < 10000000)
		{
			tmp = vs1838_edge_time[2] - vs1838_edge_time[1];
			if (tmp < 3000000)
			{
				/* 获得了重复码 */
				*val = vs1838_data;
				return 0;
			}
		}
	}

	/* 接收到了66次中断 */
	m = 3;
	if (vs1838_edge_cnt >= 68)
	{
		/* 解析到了数据 */
		for (i = 0; i < 4; i++)
		{
			data[i] = 0;
			/* 先接收到bit0 */
			for (j = 0; j < 8; j++)
			{
				/* 数值: 1 */	
				if (vs1838_edge_time[m+1] - vs1838_edge_time[m] > 1000000)
					data[i] |= (1<<j);
				m += 2;
			}
		}

		/* 检验数据 */
		data[1] = ~data[1];
		if (data[0] != data[1])
		{
			printk("%s %s line %d, %x, %x, %x\n", __FILE__, __FUNCTION__, __LINE__, data[0], data[1], ~data[1]);
			return -2;
		}

		data[3] = ~data[3];
		if (data[2] != data[3])
		{
			printk("%s %s line %d, %x, %x, %x\n", __FILE__, __FUNCTION__, __LINE__, data[2], data[3], ~data[3]);
			return -2;
		}

		vs1838_data = (data[0] << 8) | (data[2]);
		*val = vs1838_data;
		return 0;
	}
	else
	{
		/* 数据没接收完毕 */
		return -1;
	}	
}
	

static irqreturn_t vs1838_isr(int irq, void *dev_id)
{
	unsigned int val;
	int ret;
	//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 0, 0))
		vs1838_edge_time[vs1838_edge_cnt++] = ktime_get_boottime_ns();
#else
		vs1838_edge_time[vs1838_edge_cnt++] = ktime_get_boot_ns();
#endif

	/* 判断超时 */
	
	if (vs1838_edge_cnt >= 2)
	{
		if (vs1838_edge_time[vs1838_edge_cnt-1] - vs1838_edge_time[vs1838_edge_cnt-2] > 30000000)
		{
			/* 超时 */
			vs1838_edge_time[0] = vs1838_edge_time[vs1838_edge_cnt-1];
			vs1838_edge_cnt = 1;			
			return IRQ_HANDLED; // IRQ_WAKE_THREAD;
		}
	}

	ret = vs1838_parse_data(&val);
	if (!ret)
	{
		/* 解析成功 */
		vs1838_edge_cnt = 0;
		//printk("get ir code = 0x%x\n", val);
		val=val&0xff;

		/* D. 输入系统: 上报数据 */
		input_event(vs1838_input_dev, EV_KEY, val, 1);
		input_event(vs1838_input_dev, EV_KEY, val, 0);
		input_sync(vs1838_input_dev);		
		//input_event(vs1838_input_dev, EV_SYN, 0, 0);
		
		
	}
	else if (ret == -2)
	{
		/* 解析失败 */
		vs1838_edge_cnt = 0;
	}
	
	return IRQ_HANDLED; // IRQ_WAKE_THREAD;
}


/* 1. 从platform_device获得GPIO
 * 2. gpio=>irq
 * 3. request_irq
 */
static int vs1838_probe(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 1. 获得硬件信息 */
	vs1838_data_pin = gpiod_get(&pdev->dev, NULL, 0);
	if (IS_ERR(vs1838_data_pin))
	{
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	}

	irq = gpiod_to_irq(vs1838_data_pin);

	request_irq(irq, vs1838_isr, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, "vs1838", NULL);

	/* 输入系统的代码 */
	/* 参考: drivers\input\keyboard\gpio_keys.c */
	/* A. 分配input_dev */
	vs1838_input_dev = devm_input_allocate_device(&pdev->dev);

	/* B. 设置input_dev */
	vs1838_input_dev->name = "vs1838";
	vs1838_input_dev->phys = "vs1838";

	/* B.1 能产生哪类事件 */
	set_bit(EV_KEY, vs1838_input_dev->evbit);
	set_bit(EV_REP, vs1838_input_dev->evbit);

	/* B.2 能产生哪些事件 */
	//__set_bit(KEY_0, vs1838_input_dev->keybit);
	memset(vs1838_input_dev->keybit, 0xff, sizeof(vs1838_input_dev->keybit));
	
	/* C. 注册input_dev */
	input_register_device(vs1838_input_dev);

	return 0;
}

static int vs1838_remove(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	input_unregister_device(vs1838_input_dev);
	free_irq(irq, NULL);
	gpiod_put(vs1838_data_pin);
	return 0;
}



static const struct of_device_id ask100_vs1838[] = {
    { .compatible = "pgg,vs1838" },
    { },
};

/* 1. 定义platform_driver */
static struct platform_driver vs1838_driver = {
    .probe      = vs1838_probe,
    .remove     = vs1838_remove,
    .driver     = {
        .name   = "myvs1883_drv",
        .of_match_table = ask100_vs1838,
    },
};

/* 2. 在入口函数注册platform_driver */
static int __init vs1838_init(void)
{
    int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    err = platform_driver_register(&vs1838_driver); 
	return err;
}

/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 *     卸载platform_driver
 */
static void __exit vs1838_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    platform_driver_unregister(&vs1838_driver);
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(vs1838_init);
module_exit(vs1838_exit);

MODULE_LICENSE("GPL");



只需要注册驱动,注册中断即可,不再需要字符设备那些内容,就可以实现一个驱动输入。
再次重复一下
注册完成后,会在/dev/input/下出现一个新的event1,这个就是用户侧使用的文件。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第8张图片

实测

用户侧代码

int main(int argc, char **argv)
{
	int fd;
	//unsigned int data;
	struct input_event data;

	int i;
	
	/* 1. 判断参数 */
	if (argc != 2) 
	{
		printf("Usage: %s \n", argv[0]);
		return -1;
	}


	/* 2. 打开文件 */
//	fd = open(argv[1], O_RDWR | O_NONBLOCK);
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}


	while (1)
	{
		if (read(fd, &data, sizeof(data)) == sizeof(data))
		{
			printf("get IR code  :");
			printf(" Type: [0x%x],", data.type);
			printf(" Code: [0x%x],", data.code);
			printf(" Val : [0x%x]\n", data.value);
		}
		else 
		{
			printf("get IR code: -1\n");
		}
		//sleep(5);
	}
	
	close(fd);
	
	return 0;
}

找到了一个能识别的遥控器,是一个机顶盒的遥控器,

其他的遥控器,还是会报错,数据帧的校验通不过,
编译好用户程序开始测试
在这里插入图片描述
没问题,能够收到code。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第9张图片

注意事项

在系统上报数据的时候,这个val,如果超过0xff就无法上报,所以只取了低字节。

val=val&0xff;
input_event(vs1838_input_dev, EV_KEY, val, 1);

结束语

夫人带孩子回去给姥姥过生日了,突然时间就边得多了起来,不过也感觉无聊了,没有那个整天叫你的孩子,于是抓紧时间学习学习。
今天突然有了一个想法,就是以后看能不能有时间的话,再去读一次大学,换个专业学习些新的知识,不过这是个美好的想法,估计也很难实现,可能下半生还是要继续奔波劳苦了。
Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第10张图片
今天听到了一个损人的称呼,你真是三分之一的哪吒。有人知道为啥吗?Linux驱动开发(十)---树莓派输入子系统学习(红外接收)_第11张图片

你可能感兴趣的:(驱动开发,操作系统,linux知识,驱动开发,linux,树莓派,VS1883)